| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 문맥을 반영한 토픽모델링
- Roberta
- 토픽 모델링
- Optimizer
- NLP
- 붕괴 스타레일
- geocoding
- 데이터리안
- KeyBert
- 구글 스토어 리뷰
- LDA
- CTM
- 블루아카이브 토픽모델링
- 데이터넥스트레벨챌린지
- 포아송분포
- 블루 아카이브
- 클래스 분류
- 자연어 모델
- Tableu
- 트위치
- 원신
- 조축회
- 코사인 유사도
- 다항분포
- SBERT
- 개체명 인식
- BERTopic
- 옵티마이저
- 데벨챌
- 피파온라인 API
- Today
- Total
분석하고싶은코코
CNN으로 온라인 게임 어뷰저 탐지해보기 본문
이번 포스팅에서는 온라인 게임 데이터로 어뷰저를 탐지하는 방법에 대해서 이야기해보겠습니다. 사용된 데이터는 가상의 데이로 실제 게임 데이터는 아닙니다!
글에서 세세한 정보 모두를 다루지는 않고 간단하게만 다뤄보겠습니다. 순서는 EDA -> Modeling순서로 해보겠습니다.
EDA
각 피처들은 가상의 피처들이고 정확히 어떤 정보인지는 모르지만 어떤 종류의 데이터인지 정도만으로 진행하였습니다. 또한, 모든 피처를 다루지 않고 과정중 확인해본 주요 피처들에 대해서만 다루겠습니다.
이번에 사용될 데이터는 총 104,399개의 데이터로 다음과 같은 정보들을 갖고 있습니다. 캐릭터에 대한 정보와 캐릭터가 상호작용한 여러 정보들을 갖고 있는 데이터입니다. 해당 데이터 값들은 정규화가 되지 않고 각자 단위값들로 기록이 되어있습니다. 피처중 Class가 있는데 이는 어뷰저인지 아닌지를 판별하기 위한 라벨링 값입니다. 대부분이 None값을 갖고 있고 일부 데이터만 1(어뷰저), 0(일반유저)의 값을 갖고 있습니다.


어뷰저와 어뷰저가 아닌 그룹을 나눴고 ID에 중복값을 제외한 수를 카운트해보니 다음과 같이 나왔습니다. 이를 통해 알 수 있는 것은 총 10,441개의 계정 데이터가 있고 그중 937개의 어뷰징 계정, 625의 정상 계정 정보가 있음을 알 수 있습니다. 추가적으로 알 수 있는것은 평균적으로 1인당 약 10개의 캐릭터를 소유하고 있는 것도 유추해볼 수 있습니다.
ab_df['ID'].nunique(), non_ab_df['ID'].nunique(), df['ID'].nunique()
(937, 625, 10441)
우선적으로 확인해볼 것은 어뷰저들이 어떤 캐릭터(클래스)를 사용하고 있는지 입니다. 어뷰저들의 목적에는 여러가지가 존재할 수 있지만 공통적인 전제 조건은 최대한 간단하게 반복적인 활동을 단시간에 할 수 있어야한다입니다. 이를 확인하고자 각 그룹으로 나눈 데이터를 시각화 해보니 그 모습이 확실하게 들어났습니다. 순서는 각각 어뷰저, 정상, 미판별 유저의 분포 그래프입니다. 정상 유저의 경우 직업군이 고르게 분포해 있는 반면 캐릭터 수가 훨씬 많은 어뷰저 그룹군에는 5개의 특정 클래스만 분포해 있는 것을 확인할 수 있습니다. 특히 30번 클래스가 압도적이고 이는 30번 캐릭터가 어뷰징하기 가장 적합한 캐릭터임을 유추해볼 수 있습니다. 그 뒤로 29, 31번 클래스가 따르고 있습니다.
하지만 29,30,31번 클래스가 어뷰징 유저가 많다고 해서 뭔가를 할 수는 없습니다. 왜냐하면 아래 정상과 미판별 그룹군에서도 해당 직업군이 타 클래스에 비해 압도적으로 많습니다. 이를 통해 유추해볼 수 있는건 해당 클래스들이 흔히 말하는 0티어 직업군으로 분류되고 있고 많은 유저들이 보유하고 있는 캐릭터라는 것입니다. 그래서 더욱 결과 도출에 조심해야한다는 것을 알려주기도 합니다.

다음은 캐릭터의 레벨 분포입니다. 분포만 보더라도 두 그룹군에 큰 차이가 존재한다고 보이지 않습니다. 해당 피처로는 어뷰저를 구분짓기는 어려워보입니다.

소셜 데이터 정보 분포입니다. 첫 3개에 대한 피처를 보면 어뷰저는 전부 0값을 갖고 있습니다. 어뷰저가 아닌 유저들의 경우도 0에 많이 분포해 되어 있지만 그렇지 않은 피처들이 몇개 눈에 들어옵니다.

거래정보 데이터에서도 0에 많이 분포해 있지만 확실하게 어뷰징 유저와 정상 유저간의 어느정도 차이가 존재함을 알 수 있습니다. 조금 다른 부분은 첫번째 거래정보 피처가 정상 유저에 비해 높은 지표를 보이고 있는 유일한 데이터임을 확인할 수 있습니다.

행동 데이터에서도 둘의 차이가 존재합니다. 어떠한 행동인지에 대해서는 알 수 없지만 첫번째, 두번째와 마지막 데이터에서 확실한 차이가 있음을 확인할 수 있습니다.

나아가 기본적인 데이터의 정보를 확인했으니 이제 피처간 상관관계를 확인해보았습니다. 저는 그룹간 데이터 분포와 상관관계를 통해 모델링에 사용할 피처 선택의 기준으로 하였습니다. 상관관계는 각 그룹에서 진행하였습니다. 따라서 피처간 상관관계는 두 개의 값이 나왔고 이 값이 역의 관계 혹은 2배 이상의 상관을 갖는 경우 의미있는 차이가 있다 가정하여 필터링 하였습니다. 따라서 사용될 피처는 총 18개입니다.(액션 7개, 거래 2개, 소셜 3개, 캐릭터 특성 6개)
(모든 상관관계를 포스팅하기에는 반복적인 작업이라 따로 하지 않겠습니다. 직접 확인해보고 선정 기준을 정해보는 것도 좋을 것 같습니다.)
Modeling
이제 EDA를 통해 선정한 피처를 가지고 딥러닝(CNN)을 통한 어뷰저를 탐지해보겠습니다.
우선 선정된 피처들에 대해서 정규화를 진행했습니다. 또한 직업은 범주형 데이터이기에 피처로 사용하기 위해서 원-핫 인코딩을 진행하였습니다.
from sklearn.preprocessing import MinMaxScaler
import numpy as np
df = pd.read_csv('./data.csv')
min_max_col = [
# ... input selected column for modeling
]
# Min-Max 스케일러 생성
scaler = MinMaxScaler()
# 데이터를 스케일링
scaled_df = pd.DataFrame(scaler.fit_transform(df[min_max_col]), columns=min_max_col)
# 직업 원-핫 인코딩
one_hot_df = pd.get_dummies(df[['char_jobcode']], columns=['char_jobcode'])
# 정규화, 원-핫인코딩, 라벨 데이터 합치기
df = pd.concat([scaled_df, one_hot_df, df['Class']], axis=1)
모델링을 위한 데이터 처리를 완료했으니 이제 모델에 입력값으로 사용하기 위한 처리를 진행합니다. 그런데 여기서 추가적인 필터링을 진행하였습니다. 그 이유는 EDA과정에서 '주어진 어뷰저 데이터에 특정 클래스만 있었다'를 확인했기 때문에 해당 5개의 클래스로만 데이터를 축소 시켜서 해당 클래스에서만 어뷰저를 탐지를 잘 하는 모델로 만드는 접근입니다. 이렇게 한 이유는 실제로 어뷰저 탐지는 굉장히 민감한 문제입니다. 비유하자면 이는 암에 대한 진찰이 비슷한 예시일 수 있습니다. 그래서 이후 모델 평가 지표를 정확도(Accuracy)가 아닌 정밀도(Precision)을 사용하였습니다.
# Null값이 아닌 값들만 가져오기
tmp = df[~df['Class'].isna()]
# 어뷰저 정보가 있는 클래스만 걸러내기
tmp = tmp[(tmp['char_jobcode_21'] == 1) |
(tmp['char_jobcode_29'] == 1) |
(tmp['char_jobcode_30'] == 1) |
(tmp['char_jobcode_31'] == 1) |
(tmp['char_jobcode_32'] == 1)
]
X = tmp[use_col].values.tolist()
y = tmp['Class'].values
# 데이터를 훈련 및 테스트셋으로 나누기
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 데이터를 PyTorch Tensor로 변환
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.FloatTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.FloatTensor(y_test)
# 데이터 로더 생성 batch=4
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
batch_size = 4
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
모델 설계는 정말 간단한 CNN모델로 설계하였습니다. 마지막에 층에 Sigmoid 활성화 층을 쌓아 1(어뷰저)에 대한 확률을 받도록 설정하였습니다. 손실함수는 이진분류 문제로 Binary-Cross-Entropy함수를 사용하였습니다.
class DetectAbuser(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super(DetectAbuser, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_dim, output_dim)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
x = self.sigmoid(x)
return x
# 모델 초기화 및 손실 함수, 최적화기 설정
input_dim = len(X_train[0])
hidden_dim = 128
output_dim = 1
model = DetectAbuser(input_dim, hidden_dim, output_dim)
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 모델 훈련
epochs = TRAIN_EPOCH
for epoch in tqdm(range(epochs)):
for batch_X, batch_y in train_loader:
optimizer.zero_grad()
predictions = model(batch_X).squeeze(1)
loss = criterion(predictions, batch_y.float())
loss.backward()
optimizer.step()
이후 최적의 임계값을 설정하는 방법에서 고민이 많았는데 최종적으로 저는 유저의 경험에 있어서 '어뷰저로 분류된다는 것은 굉장히 민감하고 불쾌한 경험을 유발한다.'의 전제으로 최대한 오분류가 이러나지 않는 수치를 찾기 위해 지표로 정밀도로 선택하고 가장 높은 정밀도를 보여주는 임계값을 찾아 최종 모델로 선발하였습니다. 이 과정에서 오분류가 완전 일어나지 않는 경우는 제외하고 True Positive도 10개 미만인 경우도 제외하는 조건을 타협점으로 어뷰저를 탐지 임계값을 설정해보았습니다.
# 테스트 데이터에 대한 예측 및 평가
model.eval()
best_p, precision = 0, 0
best_threshold, best_tp, best_fp =0, 0, 0
for i in tqdm(range(1, 1000)):
threshold = i*0.001
tp, fp = 0, 0
with torch.no_grad():
correct = 0
total = 0
for batch_X, batch_y in test_loader:
predictions = model(batch_X).squeeze(1)
rounded_predictions = torch.Tensor([1 if prediction > threshold else 0 for prediction in predictions])
for rounded_prediction, true_y in zip(rounded_predictions, batch_y):
if rounded_prediction == 1:
ab_cnt+=1
# True Pos
if (rounded_prediction == 1) & (true_y == 1):
tp +=1
# False Pos
if (rounded_prediction==1) & (true_y == 0):
fp +=1
total += batch_y.size(0)
correct += (rounded_predictions == batch_y).sum().item()
precision = tp/(tp+fp)
if (precision == 100) & (tp <10) :
# print(f'FIND Best precision, But Sample under 10 i-> {i}')
continue
if best_p < precision:
# print(f'CHANGE : values i -> {i}')
best_p = precision
best_fp = fp
best_tp = tp
best_threshold = threshold
accuracy = correct / total
precision = best_tp/(best_tp+best_fp)
print(f'Test Accuracy: {accuracy * 100:.2f}%')
print(f'Test Precision : {precision * 100:.2f}%')
print(f'Cnt Statics : {best_threshold}, {best_tp}, {best_fp}')
결과
다음은 훈련된 모델에 가장 높은 지표를 얻을 수 있었던 임계값을 베스트를 저장한 결과로 정확도는 40.62%로 굉장히 낮게 나왔습니다. 그 이유는 어뷰저인 유저도 어뷰저가 아닌 유저로 판별했기 때문입니다. 하지만 제가 평가 지표로 삼은 정밀도는 94.74%로 굉장히 높게 나왔습니다. 실제로 19개의 어뷰저를 탐지했고 18개는 실제로 어뷰저였고 1개가 오분류된 정상유저였습니다.
Test Accuracy: 40.62%
Test Precision : 94.74%
Cnt Statics : 18, 1
마치며
실제로는 이정도 수치로는 어뷰저로 분류하기에는 어렵다고 생각합니다. 보다 높은 테스트 결과를 얻고 오분류되는 결과가 없어야할 것입니다. 이를 위해서 딥러닝 모델이 아닌 다른 접근법 혹은 다른 모델 구조를 만들어 사용해볼 수 있습니다. 혹은 전처리 과정에서 어뷰저와 정상 유저를 구분짓기 위한 피처를 추가적으로 선정할 수 있고 혹은 조합을 통해 새로운 피처를 만들어 낼 수 있습니다. 그런데 이 과정에서 주의할점은 잘못된 피처는 모델의 성능 저하의 원인이 되니 면밀한 분석이 필요한 부분입니다.
'머신러닝&딥러닝' 카테고리의 다른 글
| 로컬 서버에 LLM 올리기 - Docker, Flask, LLM (0) | 2024.04.10 |
|---|---|
| 모델 최적화를 위한 옵티마이저(Optimizer) (2) | 2024.02.14 |
| YOLOv8 object tracking(detecting) (0) | 2024.02.06 |
| QLoRA-Efficient Finetuning of Quantized LLMs (1) | 2024.01.24 |
| Pytorch - Error 정리 페이지 (0) | 2024.01.18 |