AI가 쓴 글, 어떻게 구별할까? 🤔
요즘 인공지능이 만든 글이 너무 자연스러워서 사람이 쓴 건지, AI가 쓴 건지 구분하기 어려운 경우가 많죠. 블로그 글, 뉴스 기사, 심지어 논문까지도 AI가 작성하는 시대가 왔습니다. 그런데, AI가 만든 글을 정확하게 구별할 수 있는 방법이 있을까요?
오늘은 최근 나온 논문 "Feature-Level Insights into Artificial Text Detection with Sparse Autoencoders"를 소개해 드릴게요. 이 논문에서는 Sparse Autoencoder(SAE)라는 기법을 활용해 AI가 작성한 텍스트를 효과적으로 판별하는 방법을 연구했어요. SAE를 이용하면 AI가 쓴 글에서 특정한 특징을 찾아낼 수 있다고 하는데, 과연 어떤 원리일까요? 🤔
논문 설명 동영상 : https://www.youtube.com/watch?v=UvGSv-Gn0qs

AI가 쓴 글, 뭐가 다를까? 🧐
AI가 작성한 텍스트는 자연스럽지만, 미묘한 차이가 있어요. 예를 들면:
✔️ 장황한 서론 → AI는 서론을 길게 늘어놓는 경향이 있어요. "인공지능이란 현대 사회에서 매우 중요한 기술 중 하나입니다…" 같은 문장이 흔하죠.
✔️ 불필요한 동의어 치환 → 비슷한 의미의 단어를 기계적으로 바꾸면서 문장이 어색해질 때가 있어요. 예를 들어, "이 문제는 중요하다" → *"이 사안은 중대하다"*처럼요.
✔️ 특정 문체 반복 → 논문 스타일에서는 너무 격식을 차리거나, 같은 유형의 표현을 과하게 반복하는 경우가 많아요.
그럼 이런 특징을 어떻게 찾아낼까요? 바로 Sparse Autoencoder(SAE)를 활용하면 됩니다.
Sparse Autoencoder(SAE), 뭐가 특별할까? 🤖
SAE는 쉽게 말해, "텍스트의 핵심 특징을 뽑아내는 인공지능 기법"입니다.
이게 어떻게 가능할까요?
1️⃣ 압축(인코딩) → 텍스트를 아주 작은 크기의 데이터(숫자 벡터)로 변환합니다.
2️⃣ 복원(디코딩) → 이 작은 데이터를 다시 원래의 텍스트 형태로 되돌려 봅니다.
3️⃣ 희소성(Sparsity) 적용 → 중요한 특징만 남기고, 불필요한 정보는 걸러냅니다.
이 과정에서 AI가 만든 글과 사람이 쓴 글의 차이점이 명확하게 드러나는 패턴을 찾아낼 수 있어요. 예를 들면, AI가 특정 단어를 반복적으로 쓰거나, 문장 구조가 너무 일정한 패턴을 보이는 경우가 SAE를 통해 포착됩니다.
1. 실제 데이터셋 준비하기
1) AI 생성 텍스트 수집
- GPT 계열(GPT-3.5, GPT-4, etc.), LLaMA, OPT 같은 모델을 이용해 다양한 프롬프트로 텍스트를 생성합니다.
- 도메인을 다양하게 가져가는 게 좋아요. 예: 뉴스 기사, 에세이, 기술 문서, 논문 등
- 공격 기법(패러프레이징, 단어 치환, 문장 순서 뒤바꾸기 등)을 적용해 변형된 텍스트도 수집하면, 더욱 튼튼한 검출 모델을 만들 수 있습니다.
2) 사람 작성 텍스트 수집
- 위키백과 문서, 뉴스 기사, 오픈 코퍼스(예: Common Crawl), 또는 직접 작성한 에세이 등
- 저작권에 유의하며, 가능한 한 다양한 장르와 스타일의 텍스트를 포함합니다.
3) 라벨링
- AI 생성: 1
- 사람 작성: 0
- 이렇게 라벨링한 뒤, 나중에 Sparse Autoencoder가 만든 특징(혹은 은닉벡터)을 분석할 때 “어느 쪽 텍스트인지”를 평가할 수 있습니다.
2. 텍스트 → 벡터(임베딩) 변환
SAE는 숫자 벡터를 입력으로 받습니다. 따라서 텍스트를 임베딩으로 바꿔야 해요.
- 기본 벡터화: TF-IDF, Word2Vec, GloVe 등 전통적 방법
- 딥러닝 임베딩: BERT, RoBERTa, Sentence-BERT 등 사전학습(Pretrained) 언어모델을 활용
- LLM의 내부 표현: GPT, LLaMA 같은 모델의 중간 레이어(Residual Stream)에서 뽑은 벡터
논문(“Feature-Level Insights…”)에서는 LLM의 내부 표현을 뽑아서 SAE에 적용한 예시를 보여줍니다.
3. SAE로 학습
데이터셋을 준비했다면, 위에서 사용한 코드와 같은 방식으로 진행할 수 있습니다.
- train_data, test_data를 실제 AI/사람 텍스트 임베딩으로 교체
- input_dim은 임베딩 차원에 맞게 조정 (예: 768차원)
- 나머지 하이퍼파라미터(learning_rate, hidden_dim, sparsity_weight 등)는 실험적으로 찾아보세요.
Tip: 배치 크기(batch_size), 은닉층 크기(hidden_dim), 희소성 가중치(sparsity_weight)를 다양하게 바꿔가며 결과를 비교해 보는 것이 좋습니다.
4. 성능 평가하기
**단순히 재구성 오차(MSE)**만 보면 “잘 학습됐는지” 정도는 알 수 있지만, AI 탐지를 실제로 하려면 추가 지표가 필요해요.
- 재구성 오차(Anomaly Detection 관점)
- 사람이 쓴 텍스트 vs. AI 텍스트의 재구성 오차 분포를 비교해볼 수 있음.
- 예: AI 텍스트에서 오차가 더 클 수도, 반대로 사람 텍스트에서 더 클 수도 있음.
- SAE 은닉 벡터(Encoded) → 분류 모델
- SAE의 encoded 벡터만 뽑아서 XGBoost, Random Forest, Logistic Regression 등에 입력
- AI(1)/사람(0) 라벨로 분류 정확도(Accuracy), F1 Score, ROC-AUC 등을 측정
- 논문에서도 SAE 특징을 뽑은 뒤, XGBoost를 이용해 AI 텍스트 검출 정확도를 높였다고 설명합니다.
- 해석 가능성(Interpretability)
- SAE가 뽑아낸 특징(은닉 뉴런)이 실제로 어떤 단어나 구문 패턴을 의미하는지 수동 해석 또는 LLM을 통한 설명을 시도할 수 있음.
- 예: “특정 은닉 뉴런이 수학 기호나 숫자에 반응한다”, “이 뉴런은 장황한 서론에 반응한다” 등.
1. 데이터셋 개요
- openai/gpt2-output-dataset: OpenAI에서 공개한 GPT-2 Output Dataset 중 하나
- real: 사람이 작성한 텍스트
- generated: GPT-2가 생성한 텍스트
- 우리는 이것을 **AI(1) vs. 사람(0)**으로 라벨링하여 사용
이 예시는 **소규모 버전(small)**을 사용합니다. (실제 프로젝트에서는 더 많은 데이터로 실험하세요.)
2. 코드 개요
- 데이터 불러오기: datasets 라이브러리로 Hugging Face Dataset 로드
- 전처리 & 라벨링: 사람이 쓴 텍스트 → 0, AI 텍스트 → 1
- 텍스트 임베딩: 임의로 DistilBERT를 사용 (문장 벡터화)
- SAE 학습: 임베딩을 입력으로 Sparse Autoencoder를 학습
- 은닉 벡터로 분류: SAE의 encoded 결과를 LogisticRegression 등으로 분류해 정확도 확인
주의: 아래 코드는 예시용이며, 실행 환경에 따라 수정이 필요할 수 있습니다.
!pip install datasets transformers scikit-learn torch
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModel
import tqdm
# 1) Sparse Autoencoder 정의
class SparseAutoencoder(nn.Module):
def __init__(self, input_dim, hidden_dim, sparsity_weight=1e-4):
super(SparseAutoencoder, self).__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU()
)
self.decoder = nn.Sequential(
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid()
)
self.sparsity_weight = sparsity_weight
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return encoded, decoded
def sparsity_loss(self, encoded):
return torch.mean(torch.abs(encoded))
# 2) 데이터셋 로드 (GPT-2 Output Dataset)
dataset = load_dataset("openai/gpt2-output-dataset", "small")
# small 버전 예시: train split만 존재
# 실제로는 dataset["train"]을 나눠서 train/test로 사용
all_texts = []
all_labels = []
# 이 데이터셋은 real / generated 라벨이 따로 구분되어 있음
# 'real' key에 사람이 쓴 텍스트, 'generated' key에 GPT-2 생성 텍스트
for item in dataset["train"]:
if item["text"] is not None:
all_texts.append(item["text"])
# label: 사람이 쓴(real)이면 0, AI가 쓴(generated)이면 1
label = 0 if item["meta"]["partition"] == "real" else 1
all_labels.append(label)
# 3) DistilBERT로 텍스트 임베딩 (문장 벡터)
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
model_bert = AutoModel.from_pretrained("distilbert-base-uncased")
model_bert.eval()
model_bert.cuda() # GPU 사용 가능 시
def get_sentence_embedding(text):
"""DistilBERT의 [CLS] 토큰(또는 mean-pooling)을 문장 임베딩으로 사용"""
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128)
inputs = {k: v.cuda() for k, v in inputs.items()}
with torch.no_grad():
outputs = model_bert(**inputs)
# 마지막 히든 스테이트 가져오기
last_hidden_state = outputs.last_hidden_state # (batch_size, seq_len, hidden_dim=768)
# 첫 토큰(일반적으로 CLS 토큰)만 가져오거나, 평균을 낼 수도 있음
cls_emb = last_hidden_state[:, 0, :] # shape: (1, 768)
return cls_emb.squeeze(0).cpu().numpy() # (768,)
embeddings = []
for text in tqdm.tqdm(all_texts, desc="Embedding"):
emb = get_sentence_embedding(text)
embeddings.append(emb)
embeddings = np.array(embeddings)
labels = np.array(all_labels)
# 4) Train/Test Split
train_X, test_X, train_y, test_y = train_test_split(
embeddings, labels, test_size=0.2, random_state=42
)
# 5) SAE 모델 초기화
input_dim = embeddings.shape[1] # 768 (DistilBERT 임베딩 크기)
hidden_dim = 64
sparsity_weight = 1e-4
learning_rate = 0.001
num_epochs = 20
batch_size = 32
model_sae = SparseAutoencoder(input_dim, hidden_dim, sparsity_weight).cuda()
criterion = nn.MSELoss()
optimizer = optim.Adam(model_sae.parameters(), lr=learning_rate)
# Tensor 변환
train_tensor = torch.tensor(train_X, dtype=torch.float32).cuda()
test_tensor = torch.tensor(test_X, dtype=torch.float32).cuda()
train_dataset = torch.utils.data.TensorDataset(train_tensor)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 6) SAE 학습
for epoch in range(num_epochs):
model_sae.train()
train_loss = 0.0
for batch in train_loader:
batch_data = batch[0]
optimizer.zero_grad()
encoded, decoded = model_sae(batch_data)
reconstruction_loss = criterion(decoded, batch_data)
sp_loss = model_sae.sparsity_loss(encoded)
loss = reconstruction_loss + sparsity_weight * sp_loss
loss.backward()
optimizer.step()
train_loss += loss.item() * batch_data.size(0)
train_loss /= len(train_dataset)
if (epoch+1) % 5 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}] - SAE Loss: {train_loss:.4f}")
# 7) 은닉벡터 뽑아서 분류 모델 학습
model_sae.eval()
with torch.no_grad():
train_encoded, _ = model_sae(train_tensor)
test_encoded, _ = model_sae(test_tensor)
train_encoded_np = train_encoded.cpu().numpy()
test_encoded_np = test_encoded.cpu().numpy()
# 간단히 LogisticRegression으로 분류
clf = LogisticRegression(max_iter=1000)
clf.fit(train_encoded_np, train_y)
train_preds = clf.predict(train_encoded_np)
test_preds = clf.predict(test_encoded_np)
train_acc = accuracy_score(train_y, train_preds)
test_acc = accuracy_score(test_y, test_preds)
train_f1 = f1_score(train_y, train_preds)
test_f1 = f1_score(test_y, test_preds)
print("=== Classification Results (SAE Encoded) ===")
print(f"Train Accuracy: {train_acc:.4f}, F1: {train_f1:.4f}")
print(f"Test Accuracy: {test_acc:.4f}, F1: {test_f1:.4f}")
3. 결과 해석
- SAE 학습
- DistilBERT 임베딩(768차원)을 64차원으로 압축하면서 재구성 오류를 최소화함.
- 희소성 제약(L1)을 걸어 일부 뉴런만 활성화되도록 유도.
- AI vs. 사람 텍스트 분류
- 은닉 벡터(encoded)를 Logistic Regression에 넣어 정확도와 F1 스코어를 측정.
- 예시 결과로 약 90% 전후의 정확도/ F1이 나왔다고 가정.
- 의미
- SAE를 통해 원본 임베딩의 핵심 특징을 뽑아내면, 단순히 원본 임베딩만 썼을 때보다 (경우에 따라) 더 잘 구분될 수도 있음.
- 또한 SAE 은닉 뉴런을 살펴보면, “어떤 뉴런이 AI 특유의 표현을 감지하는가?” 같은 해석이 가능.
4. 정리 & 주의사항
- 실제로 이 코드를 돌리려면 GPU 환경(Colab, Kaggle Notebook 등)에서 실행하시는 걸 권장합니다.
- 데이터셋 크기에 따라 학습 시간이 많이 걸릴 수 있습니다.
- GPT-2 Output Dataset은 GPT-2만 포함합니다. 최신 GPT-3.5나 ChatGPT, LLaMA가 만든 텍스트를 섞으면 더욱 다양한 케이스에 대응할 수 있습니다.
- 여기서는 DistilBERT로 임베딩을 뽑았지만, 다른 임베딩 모델(Sentence-BERT, RoBERTa, GPT, LLaMA 등)을 시도해볼 수도 있습니다.
- 90% 정확도라는 수치는 가상의 예시입니다. 실제 결과는 데이터 크기, 모델, 파라미터 등에 따라 달라집니다.
논문의 실험 결과는 어땠을까? 📊
연구진은 두 가지 데이터셋을 활용해 실험을 진행했어요.
📌 COLING 데이터셋 → GPT-4o, LLaMA-3 같은 최신 AI 모델이 만든 다양한 텍스트 포함
📌 RAID 데이터셋 → AI가 생성한 문장을 변형하는 다양한 공격 기법(패러프레이징, 철자 변형 등)이 적용된 데이터셋
이 데이터를 SAE로 분석한 결과,
✅ AI가 작성한 텍스트는 특정한 스타일적 특징이 반복적으로 나타남
✅ SAE를 활용하면 AI 특유의 문체를 명확하게 뽑아낼 수 있음
✅ 기존 검출 방식보다 "왜 AI가 쓴 글인지" 설명할 수 있는 해석 가능성(Interpretability)이 높음
하지만 AI도 점점 교묘해진다…! 🤯
AI가 점점 발전하면서, 단순한 방식으로는 AI 텍스트를 검출하기 어려워지고 있어요. 예를 들어,
👉 프롬프트(prompt)를 다르게 설정하면 AI가 사람처럼 더 자연스러운 문장을 만들 수 있음
👉 단순한 AI 탐지 모델로는 변화하는 AI 글쓰기 스타일을 따라잡기 어려움
그래서 이 논문에서는 AI 탐지 기술도 계속 발전해야 한다고 강조합니다. AI도 점점 더 인간답게 글을 쓰려고 하기 때문에, 단순한 검출 방식으로는 한계가 있다는 거죠.
결론 🎯
AI가 만든 텍스트가 점점 많아지고 있고, 이를 효과적으로 구별하는 기술이 중요해지고 있습니다.
✅ Sparse Autoencoder(SAE)를 활용하면 AI가 작성한 텍스트의 특징을 뽑아낼 수 있다.
✅ AI가 생성한 글은 장황한 서론, 특정한 패턴의 반복, 기계적인 동의어 치환 같은 특징이 있음.
✅ 하지만 AI도 점점 더 교묘해지고 있어서, 탐지 기술도 계속 발전해야 한다.
앞으로 AI가 더욱 자연스럽게 글을 쓰게 된다면, AI 탐지 기술이 얼마나 빠르게 적응할 수 있을지가 관건이 될 것 같네요. 여러분은 어떻게 생각하시나요? 댓글로 의견 남겨주세요! 😊
🔗 논문 원문 보기: [ https://arxiv.org/abs/2503.03601]

'IT 최신 뉴스' 카테고리의 다른 글
GitHub Copilot의 백엔드 아키텍처 설계 방법 (1) | 2025.03.27 |
---|---|
2025 미래직업보고서 - 지금 갈아타야 할 때입니다! 서두르세요 (0) | 2025.03.24 |
Google Domains(현 Squarespace) 도메인을 Vercel로 연결하기 (0) | 2025.03.04 |
16가지 AI 리드 생성 시스템 (2) | 2025.03.02 |
성공하는 AI SaaS 서비스 개발하기 (0) | 2025.02.17 |