
Python은 머신러닝에 강한 언어입니다. 머신러닝이란 AI의 기반이 되는 기술을 가리킵니다.
그렇다고는 해도 단순한 모델이라면, 프로그래밍 입문자도 바로 직접 만들 수 있습니다.
이번에는, 머신러닝 중에서도 특히 심플한 "이진 분류 모델" 의 작성을 통해,
"튜플" 이라는 데이터 구조를 함께 익혀봅시다.
| 규칙 기반 | 학습 기반 (머신러닝) | |
|---|---|---|
| 방법 | 인간이 규칙을 직접 작성 | 데이터에서 패턴을 자동 추출 |
| 코드 | if "맛없" in text: | model.predict(텍스트) |
| 유지보수 | 규칙이 늘어날수록 복잡해짐 | 데이터를 추가하면 자동 개선 |
| 예측 결과 | 항상 동일 (결정적) | 학습 데이터에 따라 달라짐 |
| 적합한 경우 | 규칙이 명확한 경우 | 패턴이 복잡·데이터가 많은 경우 |
# ❌ 규칙 기반: 단어를 일일이 if문으로 처리
def classify(text):
if "맛없" in text or "불친절" in text or "아쉽" in text:
return "부정"
elif "맛있" in text or "친절" in text or "훌륭" in text:
return "긍정"
else:
return "판단 불가" # 새로운 단어가 나올 때마다 if문 추가 필요
# ✅ 학습 기반: 데이터에서 패턴을 자동 학습
model.fit(X, labels) # 한 번 학습
predictions = model.predict(Y) # 새로운 텍스트를 자동 분류
if문의 규칙 기반은 처음에는 단순해 보이지만, 새로운 단어가 등장할 때마다 조건을 추가해야 합니다. 반면 머신러닝이라면, 데이터를 추가하기만 하면 자동으로 패턴을 업데이트합니다.
AI (인공지능)
└── 머신러닝 (기계학습)
└── 딥러닝 (심층학습)
└── LLM (대규모 언어 모델: GPT, Gemini 등)
AI라는 큰 분야 안에 머신러닝이 있고, 그 안에 딥러닝이 있습니다. AlphaGo로 유명한 딥러닝, 그리고 Gemini나 ChatGPT는 딥러닝 기반의 LLM입니다. 이번에 만드는 것은 그보다 훨씬 단순한 "통계적 분류 모델" 이지만, 원리는 같습니다. 어려운 계산은 전부 라이브러리에 맡기고, 우리는 머신러닝의 "학습 기반" 부분을 체험해봅시다.
이번에는, Scikit-learn(사이킷런)이라는 머신러닝 라이브러리를 사용하여, "긍정"과 "부정"을 분류하는 자작 AI를 만들어봅니다. 이 자작 AI를 활용하여, 공공데이터포털(data.go.kr)이나 네이버 플레이스 등에서 취득한 관광지 리뷰를 "긍정"과 "부정"으로 분류하는 것이 목표입니다.
튜플은, 여러 값을 모아 하나의 변수에 저장할 수 있는 데이터 구조입니다. 리스트와 비슷하지만, 한번 만들면 내용을 변경할 수 없다(immutable) 는 점이 특징입니다.
# 리스트: 변경 가능 (mutable)
my_list = [1, 2, 3]
my_list[0] = 100 # ✅ 가능
print(my_list) # [100, 2, 3]
# 튜플: 변경 불가 (immutable)
my_tuple = (1, 2, 3)
my_tuple[0] = 100 # ❌ TypeError: 'tuple' object does not support item assignment
리스트 [] | 딕셔너리 {} | 튜플 () | |
|---|---|---|---|
| 변경 가능 | ✅ mutable | ✅ mutable | ❌ immutable |
| 접근 방법 | 인덱스 번호 | 키 이름 | 인덱스 번호 |
| 중복 허용 | ✅ | 키는 ❌ | ✅ |
| 주요 용도 | 동적인 목록 | 속성·설정 값 | 고정 데이터셋·좌표 |
머신러닝의 훈련 데이터는 학습 후에 수정하지 않는 것이 원칙입니다. 튜플의 immutable 특성이 이를 코드 수준에서 보장합니다. "이 데이터를 실수로 바꾸지 않겠다"는 의사를 코드로 표현하는 것입니다.
# 개/고양이 울음소리 분류 데이터셋
# 1: 개 울음소리, 0: 고양이 울음소리
dataset = [
("멍멍", 1),
("야옹야옹", 0),
("왈왈", 1),
("냐옹", 0),
("낑낑", 1),
("아오~", 0),
]
# dataset[0][0] = "변경" ← ❌ 튜플 내부는 변경 불가
Scikit-learn은 Python으로 머신러닝을 수행하기 위한 라이브러리입니다. 알고리즘의 수학적 원리를 몰라도, 단 몇 줄로 분류 모델을 만들 수 있습니다.
model = MultinomialNB() # ① 모델 선택
model.fit(X, labels) # ② 학습
predictions = model.predict(Y) # ③ 예측
이 세 줄이 머신러닝의 핵심입니다.
이번에는 "형태소 분석 → 머신러닝" 흐름으로 코드를 작성합니다.
pip install scikit-learn konlpy
⚠️ Windows에서 konlpy 설치 시 주의
konlpy는 Java가 필요합니다. JDK가 없으면 먼저 https://www.oracle.com/java/technologies/downloads/에서 설치 후 진행하세요.
Mac/Linux에서는 추가 설치 없이 바로 사용 가능합니다.
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from konlpy.tag import Okt
# 데이터셋 작성
dataset = [
("기쁘다", 1), ("슬프다", 0), ("최고", 1), ("최악", 0),
("즐겁다", 1), ("싫다", 0), ("만족", 1), ("불만", 0),
("좋다", 1), ("아쉽다", 0), ("넓다", 1), ("좁다", 0),
("쾌적하다", 1), ("불쾌하다", 0), ("훌륭하다", 1), ("유감이다", 0),
("맛있다", 1), ("맛없다", 0), ("친절하다", 1), ("불친절하다", 0)
]
# 데이터셋을 텍스트와 라벨로 분할
texts, labels = zip(*dataset)
# Okt 형태소 분석기 초기화
okt = Okt()
# =============================================
# 훈련 텍스트를 형태소별로 분절하여 공백으로 결합
# =============================================
spaced_texts = []
for text in texts:
morphs = okt.morphs(text)
spaced_texts.append(" ".join(morphs))
# 훈련 텍스트를 확인
print(spaced_texts)
# 텍스트를 수치로 변환
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(spaced_texts)
# 분류 모델의 훈련
model = MultinomialNB()
model.fit(X, labels)
# ==========================================
# 리뷰를 형태소별로 분절하여 수치로 변환
# ==========================================
reviews = ["훌륭한 숙소였습니다.", "아쉬운 서비스였습니다.", "맛없는 음식이었습니다."]
spaced_reviews = []
for review in reviews:
morphs = okt.morphs(review)
spaced_reviews.append(" ".join(morphs))
# 리뷰를 수치로 변환
Y = vectorizer.transform(spaced_reviews)
# 예측 실행
predictions = model.predict(Y)
# 결과 표시
for text, label in zip(reviews, predictions):
mood = "긍정" if label == 1 else "부정"
print(f"텍스트: '{text}' → 감정: {mood}")
# 튜플의 언팩 대입
tuple_data = (1, 2)
a, b = tuple_data
print(a) # 1
print(b) # 2
# 리스트의 언팩 대입
list_data = [1, 2, 3]
x, y, z = list_data
print(x) # 1
print(y) # 2
print(z) # 3
좌변의 변수 수와 우변의 요소 수가 일치하면, 순서대로 변수에 대입됩니다.
* — 이터러블을 개별 인수로 전개def add_two(x, y):
return x + y
print(add_two(1, 2)) # 3 ← 직접 전달
args_list = [1, 2]
print(add_two(*args_list)) # 3 ← *로 [1, 2] → 1, 2 로 전개
args_tuple = (1, 2)
print(add_two(*args_tuple)) # 3 ← 튜플도 동일
*args_list는 add_two(1, 2)와 완전히 동일한 효과입니다. *가 리스트·튜플의 "포장을 벗겨서" 개별 인수로 펼쳐줍니다.
items = ["달걀", "우유", "핫케이크 믹스"]
counts = [2, 1, 1]
print(list(zip(items, counts)))
# [('달걀', 2), ('우유', 1), ('핫케이크 믹스', 1)]
zip은 같은 인덱스끼리 쌍으로 묶어 튜플의 리스트를 만듭니다.
"리스트 + 리스트 → 튜플의 리스트"가 되는 이유는, 쌍 하나하나가 ("달걀", 2)처럼 "변경 불가의 쌍"이기 때문입니다.
for문과 조합하여 동시에 꺼내기:
students = ["Alice", "Bob", "Charlie"]
test_scores = [85, 92, 78]
for student, score in zip(students, test_scores):
print(f"{student}의 점수는 {score}점입니다.")
# Alice의 점수는 85점입니다.
# Bob의 점수는 92점입니다.
# Charlie의 점수는 78점입니다.
이 패턴은 코드 마지막의 for text, label in zip(reviews, predictions): 에서도 그대로 활용됩니다.
texts, labels = zip(*dataset) — 전치 분해이것이 이번 코드의 핵심 패턴입니다. 단계별로 분해해서 이해해봅시다.
명부 예시로 먼저 이해하기:
meibo = [
("김철수", 1),
("이영희", 2),
("박민준", 3)
]
names, ids = zip(*meibo)
print(names) # ('김철수', '이영희', '박민준')
print(ids) # (1, 2, 3)
처리 순서를 단계별로 추적:
Step 1: *meibo → 리스트를 분해하여 개별 인수로 전개
zip(
("김철수", 1), ← 튜플1
("이영희", 2), ← 튜플2
("박민준", 3) ← 튜플3
)
Step 2: zip() → 각 튜플의 "같은 위치" 요소를 모음
0번째끼리(이름): ("김철수", "이영희", "박민준")
1번째끼리(ID): (1, 2, 3)
Step 3: 언팩 대입 → 결과 튜플을 변수에 분배
names = ('김철수', '이영희', '박민준')
ids = (1, 2, 3)
이 처리를 "전치(Transpose)" 라고 합니다. 행(사람별)으로 정리된 데이터를 열(속성별)로 뒤집는 조작입니다.
변환 전 (행 = 사람별): 변환 후 (열 = 속성별):
[("김철수", 1), names = ("김철수", "이영희", "박민준")
("이영희", 2), ids = (1, 2, 3)
("박민준", 3)]
데이터셋에 적용:
dataset = [
("기쁘다", 1), ("슬프다", 0), ("최고", 1), ("최악", 0), ...
]
texts, labels = zip(*dataset)
# texts: ('기쁘다', '슬프다', '최고', '최악', ...)
# labels: (1, 0, 1, 0, ...)
머신러닝은 텍스트(X)와 라벨(y)을 별도 변수로 분리해서 학습하기 때문에, 이 전치 분해가 반드시 필요합니다.
머신러닝은 영어권에서 발전한 기술이라 공백으로 단어를 구분하는 것이 전제입니다. 한국어는 공백이 없어 그대로는 사용할 수 없으므로, 형태소 분석으로 단어를 분리합니다.
훈련 텍스트:
"맛없다" → okt.morphs() → ["맛없", "다"] → " ".join() → "맛없 다"
리뷰 텍스트:
"맛없는 음식이었습니다" → ["맛없", "는", "음식", "이었", "습니다"]
→ "맛없 는 음식 이었 습니다"
# =============================================
# 훈련 텍스트를 형태소별로 분절하여 공백으로 결합
# =============================================
spaced_texts = []
for text in texts:
morphs = okt.morphs(text) # 형태소 목록으로 분해
spaced_texts.append(" ".join(morphs)) # 공백으로 결합
처리의 흐름을 조감하면, 매우 단순합니다.
① 형태소 분석 → 공백 구분 텍스트로 변환
② 공백 구분 텍스트 → 수치로 변환
③ 수치를 학습 → 분류 모델 완성
konlpy 주요 형태소 분석기 비교:
| 클래스 | 특징 | 권장 사용 |
|---|---|---|
Okt | 빠르고 간단. SNS·구어체에 강함 | 입문·일반 감성 분류 |
Kkma | 정확도 높음. 처리가 느림 | 정밀 형태소 분석 |
Komoran | 균형 잡힌 성능 | 정형 문서 분석 |
이번 코드에서는 Okt를 사용합니다.
컴퓨터는 문자를 직접 이해하지 못하므로, 숫자로 변환해줄 필요가 있습니다.
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(spaced_texts)
fit_transform()은 두 단계를 한 번에 처리합니다:
fit (학습):
전체 문장을 훑어서 등장하는 단어 목록을 만든다
["기쁘", "다", "슬프", "최고", "최악", "맛없", ...]
transform (변환):
각 문장에서 단어가 몇 번 등장했는지 수치 표로 만든다
기쁘 다 슬프 최고 최악 맛없 ...
문장1: 1 1 0 0 0 0 ... → "기쁘 다"
문장2: 0 1 1 0 0 0 ... → "슬프 다"
문장3: 0 0 0 1 0 0 ... → "최고"
...
fit_transform vs transform의 차이:
# 훈련 데이터: fit_transform (단어 목록 생성 + 수치화를 동시에)
X = vectorizer.fit_transform(spaced_texts)
# 새 데이터: transform만 (이미 만든 단어 목록 기준으로만 수치화)
Y = vectorizer.transform(spaced_reviews)
⚠️ 중요: 리뷰 데이터에는 반드시
transform만 사용합니다.
fit_transform을 쓰면 훈련 때와 다른 단어 목록이 생성되어 모델이 올바르게 동작하지 않습니다.
model = MultinomialNB()
model.fit(X, labels) # 수치화된 텍스트 + 라벨로 학습
predictions = model.predict(Y) # 새 텍스트의 감정 예측
MultinomialNB(다항 나이브 베이즈)는 텍스트 분류에 특히 효과적인 모델입니다.
| 특징 | 내용 |
|---|---|
| 원리 | "이 단어가 많이 나오면 긍정일 확률이 높다"는 통계적 계산 |
| 속도 | 매우 빠름 |
| 데이터 요구량 | 적은 데이터로도 동작 |
| 적합한 분야 | 스팸 필터, 감성 분석, 카테고리 분류 |
예측 결과 해석:
predictions = model.predict(Y)
# 예: [1, 0, 0]
for text, label in zip(reviews, predictions):
mood = "긍정" if label == 1 else "부정"
print(f"텍스트: '{text}' → 감정: {mood}")
# 텍스트: '훌륭한 숙소였습니다.' → 감정: 긍정
# 텍스트: '아쉬운 서비스였습니다.' → 감정: 부정
# 텍스트: '맛없는 음식이었습니다.' → 감정: 부정
dataset = [("기쁘다", 1), ("슬프다", 0), ...] ← 튜플 리스트
↓ zip(*dataset) + 언팩 대입
texts = ("기쁘다", "슬프다", ...) ← 텍스트 튜플
labels = (1, 0, ...) ← 라벨 튜플
↓ okt.morphs() + " ".join() [for 루프]
spaced_texts = ["기쁘 다", "슬프 다", ...] ← 공백 구분 텍스트
↓ vectorizer.fit_transform()
X = [[1,1,0,...], [0,1,1,...], ...] ← 수치 행렬 (훈련용)
↓ model.fit(X, labels)
model ← 학습 완료된 분류 모델
↓ okt.morphs() → vectorizer.transform()
Y = [[0,0,1,...], ...] ← 수치 행렬 (리뷰용)
↓ model.predict(Y)
predictions = [1, 0, 0] ← 긍정/부정 예측 결과
↓ zip(reviews, predictions)
"훌륭한 숙소였습니다." → 긍정
"아쉬운 서비스였습니다." → 부정
"맛없는 음식이었습니다." → 부정
포인트는, 훈련 텍스트와 리뷰 텍스트에 완전히 동일한 전처리를 수행한다는 것입니다.
훈련 텍스트: "맛없다"
→ okt.morphs() → ["맛없", "다"]
→ " ".join() → "맛없 다"
→ fit_transform() → [[0, 0, 1, 0, ...]] ← X
리뷰 텍스트: "맛없는 음식이었습니다."
→ okt.morphs() → ["맛없", "는", "음식", "이었", "습니다"]
→ " ".join() → "맛없 는 음식 이었 습니다"
→ transform() → [[0, 0, 1, 0, ...]] ← Y
"맛없"이라는 형태소가 공통으로 등장하기 때문에, 모델은 리뷰를 "부정"으로 분류할 수 있습니다.
이번 데이터셋은 20건으로 매우 적습니다. 실무에서 활용하려면 데이터를 늘립니다.
dataset = [
# 기존 20건
("기쁘다", 1), ("슬프다", 0), ...
# 추가: 한국 관광지·숙소 리뷰 특화 단어
("뷰 맛집", 1), ("웨이팅 길다", 0),
("가성비 최고", 1), ("주차 불편", 0),
("재방문 의사", 1), ("위생 불량", 0),
("직원 친절", 1), ("소음 심하다", 0),
("청결하다", 1), ("좁고 비싸다", 0),
("뷰가 예쁘다", 1), ("냄새가 난다", 0),
]
100건 이상의 실제 리뷰 데이터로 학습시키면, 실무 수준의 분류기를 만들 수 있습니다.
"긍정" "부정"의 분류를 if문으로 하려고 하면, 무엇을 가지고 "긍정" "부정"으로 할지, 규칙 정하기만으로도 큰일이 됩니다.
한편 머신러닝의 경우, 100%의 정확도는 아니지만, 대략 2가지로 분류하는 모델을 간단하게 만들 수 있습니다.
"AI에게 물어보면 되잖아!" 라고 생각하기 쉽지만, AI 답변은 매번 미묘하게 다르기 때문에, 같은 질문을 해도 같은 답변이 돌아온다고는 할 수 없습니다.
반면 머신러닝이라면, 반드시 "긍정" "부정"의 2가지로 분류할 수 있으며, "그 후의 처리"는 마음대로입니다. 예를 들어 자신이 운영하는 숙소에 3,000건의 리뷰가 있고, 이것들을 "포지티브" "네거티브"로 분류하고 싶은 경우, 머신러닝을 사용하면 효율적으로 분류할 수 있습니다. 정확도가 100%가 아니더라도, 포지티브 리뷰의 대략적인 비율을 알 수 있다면, 경영 판단에 활용하는 것이 가능합니다. "60%인지 63%인지는 오차 범위"라는 경우에 최적입니다.
| 개념 | 한 줄 요약 |
|---|---|
| 규칙 기반 | if문으로 인간이 규칙을 직접 작성. 유연성 낮음 |
| 학습 기반 | 데이터에서 패턴 자동 추출. 새 데이터에 자동 대응 |
튜플 () | 변경 불가(immutable). 고정 데이터셋 표현에 적합 |
| 언팩 대입 | a, b = (1, 2) — 요소를 개별 변수에 분배 |
언팩 연산자 * | func(*list) — 리스트를 개별 인수로 전개 |
zip() | 여러 리스트를 인덱스별로 묶어 튜플의 리스트 반환 |
zip(*dataset) | 데이터셋을 "행 단위 → 열 단위"로 전치(Transpose) |
okt.morphs() | 한국어 문장을 형태소 단위로 분리 |
" ".join(morphs) | 형태소 목록을 공백으로 결합하여 문자열로 |
CountVectorizer | 텍스트 → 단어 출현 횟수 수치 행렬로 변환 |
fit_transform() | 단어 목록 학습(fit) + 수치 변환(transform) 동시 처리 |
transform() | 기존 단어 목록 기준으로만 수치 변환 (fit 없음) |
MultinomialNB | 단어 빈도 기반 텍스트 분류에 특화된 나이브 베이즈 모델 |
model.fit(X, labels) | 수치화된 텍스트와 라벨로 모델 학습 |
model.predict(Y) | 학습된 모델로 새 텍스트의 감정 예측 |
©2024-2026 MDRULES.dev, Hand-crafted & made with Jaewoo Kim.
이메일문의: jaewoo@mdrules.dev
AI강의/개발/기술자문, AI 업무 자동화 컨설팅 문의: https://talk.naver.com/ct/w5umt5
AI 프롬프트 및 워크플로우 설계 대행: https://mdrules.dev
📌 머신러닝 텍스트 분류 5단계 패턴
# ① 데이터셋 정의 (튜플 리스트) dataset = [("긍정 텍스트", 1), ("부정 텍스트", 0), ...] # ② 텍스트와 라벨 분리 (전치) texts, labels = zip(*dataset) # ③ 형태소 분석 → 공백 구분 텍스트 (훈련용) spaced = [" ".join(okt.morphs(t)) for t in texts] # ④ 수치 변환(fit_transform) + 모델 학습 X = vectorizer.fit_transform(spaced) model.fit(X, labels) # ⑤ 새 텍스트 전처리(transform) + 예측 Y = vectorizer.transform([" ".join(okt.morphs(r)) for r in reviews]) predictions = model.predict(Y)