TF-IDF 알고리즘 이해
TF-IDF(Term Frequency-Inverse Document Frequency)는 문서 내에서 단어의 중요도를 수량화하는 대표적인 통계적 방법입니다. TF(단어 빈도)는 특정 문서에서 해당 단어가 얼마나 자주 등장하는지를, IDF(역문서 빈도)는 전체 문서 집합에서 해당 단어가 얼마나 희귀한지를 측정합니다.
수식으로 표현하면 다음과 같습니다:
TF(t, d) = (문서 d에서 단어 t의 출현 횟수) / (문서 d의 전체 단어 수)
IDF(t, D) = log(전체 문서 수 / 단어 t를 포함한 문서 수)
TF-IDF(t, d, D) = TF(t, d) × IDF(t, D)직접 구현한 키워드 추출기
기존 오픈소스 의존 없이 순수 Java로 TF-IDF 기반 키워드 추출 기능을 구현해보겠습니다.
핵심 인터페이스 설계
public interface TermExtractor {
/**
* 입력 텍스트에서 키워드를 추출
*/
List<TermScore> extract(String document, int topN);
}
public class TermScore {
private final String term;
private final double score;
public TermScore(String term, double score) {
this.term = term;
this.score = score;
}
// getter 메서드
public String getTerm() { return term; }
public double getScore() { return score; }
@Override
public String toString() {
return String.format("%s|%.10f", term, score);
}
}분词 및 전처리 모듈
public interface Tokenizer {
List<String> segment(String text);
}
public class SimpleTokenizer implements Tokenizer {
// 최소 길이 필터링 및 불용어 제거
private final Set<String> stopWords;
private final int minTermLength;
public SimpleTokenizer(Set<String> stopWords, int minTermLength) {
this.stopWords = stopWords != null ? stopWords : new HashSet<>();
this.minTermLength = minTermLength;
}
@Override
public List<String> segment(String text) {
// 실제 구현시 IK-Analyzer, HanLP 등 연동 가능
// 여기서는 간단한 예시로 공백 기준 분할
return Arrays.stream(text.split("[\\s,。,!?;:\"'()【】]+"))
.filter(t -> t.length() >= minTermLength)
.filter(t -> !stopWords.contains(t.toLowerCase()))
.collect(Collectors.toList());
}
}TF-IDF 계산 엔진
public class TfidfEngine {
private final Map<String, Integer> documentFrequency;
private int totalDocuments;
public TfidfEngine() {
this.documentFrequency = new HashMap<>();
this.totalDocuments = 0;
}
/**
* 코퍼스 학습: 각 단어의 문서 등장 빈도 계산
*/
public void fit(List<List<String>> corpus) {
this.totalDocuments = corpus.size();
for (List<String> doc : corpus) {
Set<String> uniqueTerms = new HashSet<>(doc);
for (String term : uniqueTerms) {
documentFrequency.merge(term, 1, Integer::sum);
}
}
}
/**
* 단일 문서의 TF-IDF 벡터 계산
*/
public Map<String, Double> calculate(List<String> terms) {
Map<String, Integer> termCounts = new HashMap<>();
for (String t : terms) {
termCounts.merge(t, 1, Integer::sum);
}
int docSize = terms.size();
Map<String, Double> tfidfVector = new HashMap<>();
for (Map.Entry<String, Integer> entry : termCounts.entrySet()) {
String term = entry.getKey();
int count = entry.getValue();
double tf = (double) count / docSize;
double idf = Math.log((double) totalDocuments / (documentFrequency.getOrDefault(term, 1) + 1));
tfidfVector.put(term, tf * idf);
}
return tfidfVector;
}
}키워드 추출기 최종 구현
public class TfidfKeywordExtractor implements TermExtractor {
private final Tokenizer tokenizer;
private final TfidfEngine engine;
public TfidfKeywordExtractor(Tokenizer tokenizer) {
this.tokenizer = tokenizer;
this.engine = new TfidfEngine();
}
/**
* 코퍼스로 IDF 사전 구축
*/
public void buildIdf(List<String> rawCorpus) {
List<List<String>> tokenized = rawCorpus.stream()
.map(tokenizer::segment)
.collect(Collectors.toList());
engine.fit(tokenized);
}
@Override
public List<TermScore> extract(String document, int topN) {
List<String> tokens = tokenizer.segment(document);
Map<String, Double> scores = engine.calculate(tokens);
return scores.entrySet().stream()
.map(e -> new TermScore(e.getKey(), e.getValue()))
.sorted(Comparator.comparing(TermScore::getScore).reversed())
.limit(topN)
.collect(Collectors.toList());
}
}실전 활용 예시
public class KeywordDemo {
public static void main(String[] args) {
// 불용어 사전 준비
Set<String> stopWords = new HashSet<>(Arrays.asList(
"은", "는", "이", "가", "을", "를", "의", "에", "에서", "이다"
));
Tokenizer tok = new SimpleTokenizer(stopWords, 2);
TfidfKeywordExtractor extractor = new TfidfKeywordExtractor(tok);
// IDF 사전 구축용 샘플 코퍼스
List<String> corpus = Arrays.asList(
"자연어 처리는 인공지능의 한 분야입니다",
"머신러닝과 딥러닝을 활용한 자연어 처리 기술",
"키워드 추출은 문서 분석의 핵심 과정입니다"
);
extractor.buildIdf(corpus);
// 실제 키워드 추출
String target = "첨단 자연어 처리 기술로 문서에서 핵심 키워드를 자동 추출하는 방법";
List<TermScore> keywords = extractor.extract(target, 5);
keywords.forEach(System.out::println);
// 출력 예: 자연어|0.2345678901, 키워드|0.1987654321, ...
}
}성능 최적화 포인트
대규모 문서 처리 시 다음 기법들을 고려하세요:
- 역색인 구조: 단어 → 문서 ID 매핑으로 IDF 계산 가속
- 병렬 스트림: Java 8의
parallelStream()으로 다문서 동시 처리 - 캐싱: 자주 조회하는 문서의 TF-IDF 결과 메모이제이션
- 임계값 필터링: 점수 하한선으로 의미 없는 후보 제거
확장 방향
기본 TF-IDF를 넘어서는 개선안:
| 기법 | 설명 |
|---|---|
| TF-IDF + TextRank | 그래프 기반 순위 결합 |
| BM25 | 문서 길이 정규화 개선 |
| 키프레이즈 추출 | 연속된 N-gram을 하나의 키워드로 |
| 분야 특화 IDF | 도메인별 말뭉치로 사전 구축 |