Text Classification과 Sentiment Analysis

인공지능응용 · 14주차

이번 주 목표 · 토큰화부터 예측까지 데이터가 어떻게 바뀌는지 설명합니다.

실행: Colab에서 내 Drive에 사본을 저장한 뒤 Code Cell의 ▶ 또는 Shift+Enter로 위에서부터 실행하세요. ⟦직접 입력⟧ 표시가 있는 Text Cell은 더블클릭해서 수정합니다.

1. 문장의 감정을 분류하는 과정

문장의 감정을 분류하려면 먼저 단어를 숫자로 표현하고, 단어들의 정보를 모아 문장 전체를 나타내야 합니다. 문장 → Token과 Index → Embedding → 문장 벡터 → Sentiment Classification 순서로 처리합니다. 짧은 영어 Review 12개로 각 단계의 입력과 출력을 확인하며, 정답은 0: negative, 1: neutral, 2: positive입니다.

처리 단계 사용하는 코드 확인할 결과
Text를 계산할 수 있는 형태로 바꾸기 lower().split()과 vocab 단어 목록과 각 단어의 Index
길이가 다른 문장을 Batch로 묶기 encode() PAD를 포함한 (N,S) 정수 Tensor
단어를 벡터로 표현하기 nn.Embedding (N,S,E) 실수 Tensor
문장 전체의 정보를 모으기 Mask를 적용한 Mean Pooling 문장마다 벡터 하나, (N,E)
문장의 Class 판정하기 nn.Linear와 argmax (N,3) Logit과 문장별 예측 하나

N은 문장 수, S는 Batch에서 맞춘 문장 길이, E는 단어 벡터의 크기입니다. 이번에는 E=16을 사용합니다.

IMDB 예제에서 배운 개념과 이번 실습

강의 영상의 IMDB Review Classification은 영화 리뷰처럼 길이가 다른 문장을 받아, 문장 전체에 Label 하나를 붙이는 문제를 보여줍니다. 여기서 Digitalize는 글을 계산 가능한 번호로 바꾸는 과정, Vocabulary는 단어와 번호의 대응표, Embedding은 그 번호로 학습 가능한 벡터를 찾는 Lookup Table입니다. 사용하는 도구가 달라져도 문장을 Neural Network에 넣으려면 필요한 역할들입니다.

Text 처리 도구는 Dataset 읽기, Tokenization, Vocabulary 생성처럼 여러 실험에서 반복되는 작업을 묶어 제공해 왔습니다. 이런 역할을 맡던 TorchText는 개발이 종료되어, 예전 예제의 패키지 조합을 현재 환경에서 그대로 사용하는 데 제약이 있습니다. 이번에는 Python의 문자열 처리와 사전으로 전처리를 작성해, 각 단계의 결과를 직접 확인합니다. TorchText 공식 안내

실습 데이터도 짧은 Review 12개와 세 감정 Label로 줄였습니다. 문장 길이와 단어를 직접 바꾸며 Token·PAD·예측이 어떻게 달라지는지 살펴보기 위해서입니다. IMDB처럼 긴 리뷰를 다룰 때도 이 과정을 바탕으로 데이터 처리와 모델을 확장할 수 있습니다. Mean Pooling은 단어 벡터에서 문장 벡터가 만들어지는 과정을 보기 위해 선택한 단순한 모델입니다. 라이브러리의 지원 여부와 별개로, 학습 목적에 맞게 데이터와 모델의 복잡도를 정한 것입니다.

2. 12·13주차와 무엇이 달라질까?

12·13주차에서 다룬 다음 문자 예측을 짧은 예로 비교하겠습니다. hello를 사용하면 각 위치의 다음 문자를 정답으로 둡니다. Input이 hell이면 Target은 ello입니다. 문장 분류의 예에서는 great battery라는 문장 전체에 감정 Label 하나가 붙습니다.

비교 12·13주차: 다음 문자 예측 14주차: 문장 분류
한 Token 문자 하나 공백으로 나눈 단어 하나
정답 각 시점의 다음 문자 Index (N,S) 문장마다 감정 Index (N,)
모델 출력 시점별 문자 점수 (N,S,C) 문장별 감정 점수 (N,3)
문맥을 처리하는 방법 RNN·LSTM·GRU가 순서대로 처리 단어 벡터의 평균으로 문장 요약

RNN·LSTM·GRU는 단어를 순서대로 읽으면서 State에 문맥을 모을 수 있습니다. Mean Pooling은 각 단어의 벡터를 평균 내어 문장 벡터 하나를 만듭니다. 두 방식 모두 문장 분류에 사용할 수 있으며, 이번에는 계산 과정을 쉽게 확인할 수 있는 Mean Pooling으로 시작합니다. 이후 단어 순서를 바꿔 보면 이 단순화로 어떤 정보가 사라지는지도 알 수 있습니다.

3. 한 문장을 Index로 바꾸기

great battery는 ['great', 'battery']라는 Token 목록으로 나뉩니다. vocab은 각 단어에 정수 번호를 붙인 사전입니다. 번호의 크기가 긍정·부정의 정도를 뜻하지는 않습니다.

다음은 설명용 사전입니다. 실제 Notebook의 번호는 vocab을 출력해 확인하세요.

Token 설명용 Index 역할
PAD 0 길이를 맞추기 위한 빈자리
UNK 1 사전에 없는 단어
great 2 실제 단어
battery 3 실제 단어
bad 4 실제 단어

great battery와 bad를 묶으면 Index는 [[2,3],[4,0]]이 됩니다. excellent battery처럼 처음 보는 단어가 있으면 [1,3]으로 바꿉니다. PAD는 읽을 단어가 없는 자리이고, UNK는 단어가 있지만 모르는 경우입니다.

전처리 코드 세 줄 읽기
  • text.lower().split()은 소문자로 바꾼 뒤 공백으로 단어를 나눕니다.
  • vocab.get(word, 1)은 사전의 번호를 찾고, 없으면 UNK 번호 1을 반환합니다.
  • 짧은 문장 뒤에 0을 붙여 같은 Batch의 문장 길이를 맞춥니다.

이 Tokenizer는 단순한 영어 예제용입니다. 문장부호를 자동으로 분리하지 않으므로 battery와 battery!는 다르게 처리됩니다. 처음 직접 작성할 문장은 학습 사전의 단어로 만들고, 그다음 모르는 단어나 문장부호를 하나 넣어 차이를 확인하세요.

Vocabulary는 학습 데이터로 한 번 만듭니다. 새 문장을 예측할 때 사전을 다시 만들면 같은 Index가 다른 단어를 가리킬 수 있으므로 기존 사전을 사용합니다.

4. Embedding은 단어 번호로 벡터를 찾는다

nn.Embedding(len(vocab),16)은 단어마다 길이 16의 벡터를 저장한 표입니다. Index Tensor (N,S)를 넣으면 해당 행을 찾아 (N,S,16)을 돌려줍니다. Index에는 정수형 torch.long을 사용합니다.

12·13주차의 One-hot은 문자 위치 하나만 1인 고정된 표현이었습니다. 단어 종류가 많아지면 One-hot의 길이도 그만큼 늘어납니다. Embedding은 단어마다 정해진 길이의 벡터를 학습해 사용할 수 있게 합니다. 단어 번호를 곱셈에 사용하는 대신, 그 번호에 해당하는 행을 꺼내는 이유입니다. 이번 Embedding 벡터는 처음에는 초기값으로 시작하며, Loss를 줄이는 과정에서 분류기와 함께 학습됩니다. nn.Embedding을 만들었다는 사실만으로 단어의 의미를 이미 알고 있는 것은 아닙니다.

같은 단어는 같은 벡터일까?

이 Embedding Layer에서는 같은 단어 Index가 항상 같은 행을 가리킵니다. 따라서 같은 학습 단계에서는 문장 안 위치가 달라도 같은 벡터를 꺼냅니다. 학습으로 표의 값이 갱신되면 그 단어의 벡터도 바뀝니다.

5. PAD를 빼고 문장 벡터 만들기

단어가 세 개이면 벡터 세 개를 더한 뒤 3으로 나눕니다. PAD가 하나 붙어 길이가 4가 되었더라도 실제 단어 수는 3입니다. PAD의 벡터가 0이어도 4로 나누면 평균이 작아집니다.

예를 들어 실제 단어 벡터가 [2,0], [0,2]이고 PAD가 [0,0]이면, 올바른 평균은 [1,1]입니다. PAD까지 개수에 넣으면 [2/3,2/3]으로 달라집니다.

mask = (ids != 0).unsqueeze(-1)       # 실제 단어: True, PAD: False
embedded = model.embedding(ids)      # (N,S,E)
pooled = (embedded * mask).sum(1) / mask.sum(1).clamp_min(1)

sum(1)은 문장 안 단어 축을 더합니다. unsqueeze(-1)은 Mask를 (N,S,1)로 만들어 한 단어 벡터의 모든 성분에 같은 선택을 적용하게 합니다. 결과는 문장당 벡터 하나인 (N,E)입니다.

실제로 Padding 효과를 보려면

기준 학습 문장 중 screen size okay는 세 단어여서 네 단어 문장과 묶으면 PAD가 하나 붙습니다. Notebook의 비교 예에서는 길이가 다른 great battery와 great battery bright screen을 사용합니다. 같은 짧은 문장을 혼자 처리한 결과와 긴 문장과 묶어 처리한 결과가 Mask를 적용했을 때 같은지 확인하세요.

clamp_min(1)은 나눗셈의 분모가 0이 되는 것을 막습니다. 빈 문장을 의미 있는 문장으로 만들어 주지는 않습니다. 이 예제의 encode()는 빈 문장을 UNK 하나로 처리합니다.

6. 문장 벡터에서 감정 판정까지

Linear(16,3)은 문장 벡터를 세 Class의 Logit으로 바꿉니다. 학습에서는 CrossEntropyLoss에 Logit과 정답 Index를 넣습니다. softmax는 결과를 확률 형태로 보여줄 때, argmax는 가장 큰 점수의 Class를 고를 때 사용합니다. 이는 8주차의 Multiclass Classification과 같은 방식입니다.

Embedding과 Linear의 Parameter를 함께 갱신하되, 전처리 사전과 Label의 뜻은 고정합니다. 이 데이터에서 얻은 Training Accuracy는 12개 학습 문장을 얼마나 잘 맞혔는지를 나타냅니다.

Mean Pooling의 한계 확인하기

great battery와 battery great는 단어 벡터의 합이 같아 같은 문장 벡터가 됩니다. 따라서 이 모델은 단어 순서에 따른 의미 차이를 구별하지 못합니다. 두 입력의 Logit을 직접 비교해 보세요. 12·13주차의 RNN/LSTM이 순서대로 입력을 처리한 이유와 비교하면 이 모델의 한계가 분명해집니다.

모르는 단어를 UNK로 처리해도 그 뜻을 알아내는 것은 아닙니다. 예측값만 보지 말고 실제 Token과 Index도 함께 확인하세요.

전처리 도구가 바뀌면 무엇을 확인할까?

함수 이름보다 역할을 기준으로 코드를 읽어 보세요. 새로운 Tokenizer를 사용한다면 great battery!를 어떤 Token으로 나누는지 먼저 확인합니다. Vocabulary가 달라지면 PAD·UNK 번호와 Embedding의 행 수를 확인하고, 모델 출력이 여전히 문장별 Class 점수인지 점검합니다.

구현 도구가 바뀌어도 문장을 계산 가능한 표현으로 바꾸고, 그 표현으로 Label을 예측한다는 핵심은 남습니다. 새 API를 찾는 것에 더해, 그 API가 입력을 어떻게 바꾸는지 설명하고 결과를 확인할 수 있어야 합니다. 이번 실습에서 Token·Index·shape를 단계마다 출력하는 이유도 여기에 있습니다.

학습 내용 확인

  1. 12·13주차와 달리 문장당 정답이 하나인 이유를 설명할 수 있나요?
  2. PAD와 UNK를 구별하고 각 단계의 shape를 따라갈 수 있나요?
  3. 이 모델이 단어 순서를 구별하지 못하는 이유와 Training Accuracy의 의미를 설명할 수 있나요?

PyTorch Embedding · PyTorch CrossEntropyLoss

실습 자료