# Machine Learing for CIC-IDS 2018 Dataset
## CIC-IDS 2018 Dataset
Là một bộ dữ liệu được xây dựng bởi Canadian Institute for Cybersecurity tại Đại học New Brunswick, nhằm phục vụ cho nghiên cứu và phát triển các hệ thống phát hiện xâm nhập (Intrusion Detection Systems - IDS) và các kỹ thuật bảo mật máy học. Đây là bộ Dataset về Traffic Network Flow, được thiết kế để mô phỏng một môi trường mạng thực tế, chứa cả lưu lượng hợp lệ (benign) và lưu lượng độc hại (malicious), với nhiều loại tấn công mạng khác nhau.
Để xây dựng bộ Dataset phục vụ cho bài này, ta sẽ dùng 2 bộ csv sau: ``Friday-02-03-2018_TrafficForML_CICFlowMeter.csv`` và ``Friday-16-02-2018_TrafficForML_CICFlowMeter.csv``. Lý do là vì các tệp khác có nhãn cực kỳ mất cân bằng, đòi hỏi phải có các phương pháp phát hiện bất thường. Các tệp đã chọn chứa các nhãn sau:
- Benign
- Bot
- DoS attacks-SlowHTTPTest
- DoS attacks-Hulk
Các nhãn "Bot", "DoS attacks-SlowHTTPTest" và "DoS attacks-Hulk" được kết hợp thành một nhãn "Malicious". Sau khi loại bỏ một số cột, giá trị bị thiếu và bản ghi trùng lặp, ta thu được tập dữ liệu đã xử lý ``processed_friday_dataset.csv`` kết thúc với 1.074.342 bản ghi ``Benign`` và 290.089 bản ghi ``Malicious``. Hai nhãn này sẽ được chuyển thành 0 cho Benign và 1 cho Malicious .
Vì sự thiếu hụt về cấu hình máy @@ nên để phục vụ cho mục đích nghiên cứu, ta sẽ chỉ lấy khoảng 1/10 bản ghi của tập Dataset trên. Sau đó, tập dữ liệu được chia thành 2 phần Train/Test 70/30, và cuối cùng sử dụng 2 thuật toán huấn luyện là Random Forest Classifier và XGBoots để training ra model phục vụ cho các bước sau này
## Training Random Forest Classifier
### Import thư viện cần thiết và đọc dataset
```python=
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import joblib
import sklearn
from sklearn.preprocessing import MinMaxScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay
from sklearn.utils import class_weight # For balanced class weighted classification training
# For reproducible results
RANDOM_STATE_SEED = 420
```
Import các thư viện cần thiết:
- `pandas`, `numpy`: xử lý dữ liệu.
- `matplotlib`: vẽ biểu đồ.
- `joblib`: lưu và sử dụng model
- `sklearn`: các công cụ machine learning (tiền xử lý, huấn luyện, đánh giá, ...)
- `RANDOM_STATE_SEED` = 420: đảm bảo kết quả tái lập được bằng cách cố định seed ngẫu nhiên.
Sau đó đọc dataset, kiểm tra thông tin dataset
```python=
df_dataset = pd.read_csv("dataset_mini.csv")
df_dataset
df_dataset.info()
```

### Chia thành các tập train/test
```python=
train, test = train_test_split(df_dataset, test_size=0.3, random_state=RANDOM_STATE_SEED)
```
Sử dụng train_test_split để chia tập dữ liệu gốc df_dataset thành:
- train: 70%
- test: 30%
``random_state=RANDOM_STATE_SEED``: giữ kết quả chia cố định giữa các lần chạy.

Thực hiện MinMaxScaler. Đây là một kỹ thuật chuẩn hóa dữ liệu – đưa giá trị số của từng đặc trưng (feature) về khoảng cố định, Thường sẽ là ``[0, 1]``.
Chúng ta sẽ chọn tất cả các cột, trừ 3 cột cuối cùng (vì là nơi chứa nhãn hoặc các trường đặc biệt). Và tiến hành chuẩn hóa dữ liệu của hai tập train/test bằng đoạn code dưới đây
```python=
numerical_cols = train.columns[:-3]
min_max_scaler = MinMaxScaler().fit(train[numerical_cols])
train[numerical_cols] = min_max_scaler.transform(train[numerical_cols])
test[numerical_cols] = min_max_scaler.transform(test[numerical_cols])
```
Kết quả sau chuẩn hóa:

Kiểm tra tổng thể nhãn các tập

### Tách X-train, Y-train, X-test, Y-test
Dùng ``.pop("Label")`` để:
- Lấy cột Label làm nhãn (y_train)
- Đồng thời xóa luôn cột đó khỏi train → train giờ chỉ chứa đặc trưng (X_train)
Sau đó chuyển về numpy array để dễ huấn luyện.
```python=
y_train = np.array(train.pop("Label")) # pop removes "Label" from the dataframe
X_train = train.values
print(type(X_train))
print(type(y_train))
print(X_train.shape)
print(y_train.shape)
```
Áp dụng tương tự cho phần test:
```python=
y_test = np.array(test.pop("Label"))
X_test = test.values
print(type(X_test))
print(type(y_test))
print(X_test.shape)
print(y_test.shape)
```

### Fitting Random Forest Model
Đầu tiên cần tính Class Weight. Giá trị này nhằm mục đích đảm bảo sự cân bằng cho mô hình khi khi dữ liệu huấn luyện bị mất cân bằng nhãn – ví dụ như khi có rất nhiều mẫu "0" nhưng rất ít mẫu "1".
```python=
# Calculating class weights for balanced class weighted classifier training
class_weights = class_weight.compute_class_weight(
class_weight='balanced',
classes=np.unique(y_train),
y=y_train
)
print(class_weights)
# Must be in dict format for scikitlearn
class_weights = {
0: class_weights[0],
1: class_weights[1]
}
print(class_weights)
```

Phần tiếp theo là khởi tạo mô hình
```python=
model = RandomForestClassifier(
n_estimators=100,
criterion='gini',
max_depth=None,
min_samples_split=2,
min_samples_leaf=1,
min_weight_fraction_leaf=0.0,
max_features='sqrt',
max_leaf_nodes=None,
min_impurity_decrease=0.0,
bootstrap=True,
oob_score=False,
n_jobs=None,
random_state=None,
verbose=0,
warm_start=False,
class_weight=class_weights,
ccp_alpha=0.0,
max_samples=None
)
hyperparameters = {
'n_estimators': [50, 75, 100, 125, 150]
}
```
- ``n_estimators=100``: Số lượng cây quyết định trong rừng. Càng nhiều cây, mô hình càng ổn định nhưng chậm hơn.
- ``criterion='gini'``: Tiêu chí dùng để đánh giá mức độ phân chia trong cây. 'gini' là hệ số Gini (Gini impurity), có thể dùng 'entropy' nếu bạn muốn dùng thông tin entropy.
- ``max_depth=None``: Không giới hạn độ sâu của cây. Nếu muốn tránh overfitting, bạn có thể set giá trị này.
- ``min_samples_split=2``: Số lượng mẫu tối thiểu để chia một node.
- ``min_samples_leaf=1``: Số mẫu tối thiểu ở một lá (leaf node).
- ``min_weight_fraction_leaf=0.0``: Phần trăm trọng số tối thiểu ở mỗi lá (nếu bạn dùng sample weights).
- ``max_features='sqrt'``: Số lượng đặc trưng xem xét tại mỗi node: 'sqrt' = căn bậc hai số đặc trưng. Đây là default tốt cho classification.
- ``max_leaf_nodes=None``: Không giới hạn số lượng node lá.
- ``min_impurity_decrease=0.0``: Một ngưỡng để quyết định có nên chia node hay không, dựa vào mức độ giảm impurity.
- ``bootstrap=True``: Có dùng bootstrap sampling hay không. Đây là đặc trưng của Random Forest.
- ``oob_score=False``: Có tính điểm "out-of-bag" để đánh giá mô hình hay không. Hữu ích nếu bạn không dùng cross-validation.
- ``n_jobs=None``: Số lượng luồng xử lý song song. -1 sẽ dùng tất cả CPU.
- ``random_state=None``: Nếu bạn muốn tái lập kết quả (reproducible), set một số cụ thể ở đây.
- ``verbose=0``: Mức độ in log trong quá trình chạy.
- ``warm_start=False``: Nếu True, mô hình sẽ giữ các cây cũ và thêm cây mới – hữu ích khi bạn muốn huấn luyện dần dần.
- ``class_weight=class_weights``: Chính là dictionary trọng số lớp bạn tính ở cell trước. Điều này giúp xử lý dữ liệu mất cân bằng.
- ``ccp_alpha=0.0``: Tham số cho pruning (cắt tỉa cây) theo cost-complexity. Mặc định là không cắt tỉa.
- ``max_samples=None``: Nếu bạn dùng bootstrap, bạn có thể chỉ định % dữ liệu dùng cho mỗi cây (ví dụ: 0.8).
- ``hyperparameters``: Đây là một dictionary (từ điển) dùng để chỉ định các giá trị thử nghiệm cho các siêu tham số (hyperparameters) của mô hình trong quá trình tối ưu hóa (tuning).
```python=
clf = GridSearchCV(
estimator=model,
param_grid=hyperparameters,
cv=5,
verbose=1,
n_jobs=-1
)
```
- ``estimator``: Mô hình cần tối ưu, ví dụ RandomForestClassifier.
- ``param_grid``: Tập các giá trị siêu tham số cần thử nghiệm (dạng dictionary).
- ``cv``: Số lần cross-validation, giúp đánh giá mô hình trên nhiều tập con khác nhau (ở đây là 5-fold).
- ``verbose``: Mức độ hiển thị log trong quá trình chạy (1 = hiển thị thông tin cơ bản).
- ``n_jobs``: Số lượng CPU sử dụng, -1 nghĩa là dùng toàn bộ lõi xử lý để chạy nhanh hơn.
Cuối cùng thực hiện huấn luyện và tìm ra mô hình tốt nhất
```python=
clf.fit(X=X_train, y=y_train)
```

### Chọn ra mô hình được huấn luyện tốt nhất
Kiểm tra các chỉ số của các mô hình tiềm năng sau quá trình huấn luyện
```python=
print("Accuracy score on Validation set: \n")
print(clf.best_score_ )
print("---------------")
print("Best performing hyperparameters on Validation set: ")
print(clf.best_params_)
print("---------------")
print(clf.best_estimator_)
```
Chọn mô hình tốt nhất để sử dụng vào tập test:
```python=
model = clf.best_estimator_
```

### Đánh giá mô hình trên tập test
Áp dụng mô hình vào tập test
```python=
predictions = model.predict(X_test)
```
#### Kiểm tra độ chính xác:
```python=
accuracy_score(y_test, predictions)
```

#### Đánh giá dựa trên ma trận nhầm lẫn:
```python=
cm = confusion_matrix(y_test, predictions)
print(cm)
```
Kết quả:
```
[[32027 19]
[ 8 8879]]
```
- TN (True Negative) = 32027: mẫu thuộc lớp 0, được dự đoán đúng là 0.
- FP (False Positive) = 19: mẫu thuộc lớp 0, nhưng bị dự đoán sai là 1.
- FN (False Negative) = 8: mẫu thuộc lớp 1, nhưng bị dự đoán sai là 0.
- TP (True Positive) = 8879: mẫu thuộc lớp 1, được dự đoán đúng là 1.

#### Classification report:
Đây là một bảng tóm tắt các chỉ số đánh giá hiệu suất mô hình phân loại trên từng lớp.
- Precision: Tỷ lệ dự đoán đúng trong số các dự đoán là lớp đó. (TP / (TP + FP))
- Recall: Tỷ lệ mẫu thực sự thuộc lớp đó được mô hình nhận diện đúng. (TP / (TP + FN))
- F1-score: Trung bình điều hòa giữa precision và recall. (2 * P * R / (P + R))
- Support: Số lượng mẫu thật sự thuộc về từng lớp trong tập test.
- Accuracy: Tỷ lệ dự đoán đúng trên toàn bộ dữ liệu.
- Macro avg: Trung bình không trọng số của các chỉ số giữa các lớp.
Weighted avg: Trung bình có trọng số theo số mẫu (support) của từng lớp.

Phân tích kết quả:
Lớp 0 (class 0):
- Precision: 0.99975 → 99.975% dự đoán là lớp 0 là đúng.
- Recall: 0.99941 → Mô hình nhận diện được 99.941% mẫu thật sự là lớp 0.
- F1-score: 0.99958 → Cân bằng giữa precision và recall.
- Support: 32,046 mẫu thực sự thuộc lớp 0.
Lớp 1 (class 1):
- Precision: 0.99786 → 99.786% dự đoán là lớp 1 là đúng.
- Recall: 0.99910 → Mô hình phát hiện được 99.910% mẫu lớp 1.
- F1-score: 0.99848 → Rất tốt.
- Support: 8,887 mẫu thật sự thuộc lớp 1.
Toàn bộ mô hình:
- Accuracy: 0.99934 → Dự đoán đúng 99.934% tổng số mẫu.
- Macro avg: Trung bình đều giữa 2 lớp → 99.803%–99.925%.
- Weighted avg: Trung bình có tính đến số lượng mẫu mỗi lớp → 99.934%.
### Lưu và sử dụng model
Lưu model:
```python=
joblib.dump(model, "trained_models/random-forest-classifier.pkl")
```
Để sử dụng model được lưu, ta sẽ dùng ``joblist`` để load model trên
```python=
model = joblib.load("trained_models/random-forest-classifier.pkl")
```

Sau khi load được model, thực hiện dự đoán bằng cách tương tự như khi áp dụng model trên tập test:
- Input đầu vào là tập các dòng trong csv đã được loại bỏ nhãn
- Dùng ``model.predict()`` để dự đoán kết quả
Mình sẽ lấy khoảng 10 dòng trong dataset gốc, với tỉ lệ nhãn 5/5, làm xáo trộn, sau đó dự đoán và so sánh kết quả gốc:
```python=
#Read CSV Target
df = pd.read_csv("dataset_mini.csv")
# Random 10 line - 5 True 5 False
df_0 = df[df["Label"] == 0].sample(n=5, random_state=42)
df_1 = df[df["Label"] == 1].sample(n=5, random_state=42)
# Shuffle
df_sample = pd.concat([df_0, df_1]).sample(frac=1, random_state=42)
# remove label
X = df_sample.drop(columns=["Label"])
y_true = df_sample["Label"]
# forecast
y_pred = model.predict(X.values)
# result forecast
print("Forecast:", y_pred)
print("Reality:", y_true.values)
print("Accuracy:", accuracy_score(y_true, y_pred))
```
Kết quả dự đoán của mô hình có tỉ lệ chính xác khoảng 50%. Điều này xảy ra sở dĩ do dataset ban đầu đưa vào đã bị lược bớt đi để phù hợp với cấu hình máy

## Training XGBoots Classifier
Chúng ta chỉ cần đổi thuật toán từ Random Forest sang XGBoots ở phần Fitting, các phần còn lại như chia tập dữ liệu, áp dụng vào tập test, thử nghiệm model có thể giữ nguyên.
```python=
model = XGBClassifier(
n_estimators=100, # Số lượng cây (boosting rounds)
max_depth=6, # Độ sâu tối đa của mỗi cây
learning_rate=0.1, # Tốc độ học (step size shrinkage)
subsample=1.0, # Tỷ lệ dữ liệu sử dụng cho mỗi cây (row sampling)
colsample_bytree=1.0, # Tỷ lệ cột sử dụng cho mỗi cây (feature sampling)
gamma=0, # Giới hạn để giảm độ phức tạp (cắt tỉa cây)
scale_pos_weight=1, # Trọng số để cân bằng lớp dương (dùng cho dữ liệu mất cân bằng)
n_jobs=-1, # Sử dụng toàn bộ CPU để tăng tốc
random_state=42, # Đảm bảo tái lập kết quả
verbosity=0 # Mức độ hiển thị log
)
hyperparameters = {
'n_estimators': [100, 200],
'max_depth': [3, 5, 7],
'learning_rate': [0.01, 0.1, 0.2],
'subsample': [0.8, 1.0],
'colsample_bytree': [0.8, 1.0],
'gamma': [0, 1],
'scale_pos_weight': [1, 2] # nếu dữ liệu mất cân bằng
}
```

Model thu được

Kết quả dự đoán có tỉ lệ chính xác khoảng 50%. Điều này xảy ra cũng là do dataset ban đầu đưa vào đã bị lược bớt đi để phù hợp với cấu hình máy
