本文目录导读:

我来详细介绍如何在PHP项目中实现TF-IDF和文本相似度计算。
TF-IDF基本概念
TF-IDF = TF(词频) × IDF(逆文档频率)
- TF(Term Frequency):词在文档中出现的频率
- IDF(Inverse Document Frequency):包含该词的文档占总文档数的比例的对数
PHP实现TF-IDF
基础类结构
<?php
class TFIDF {
private $documents = [];
private $processedDocs = [];
private $vocabulary = [];
private $idfCache = [];
public function __construct(array $documents = []) {
$this->documents = $documents;
$this->processDocuments();
}
/**
* 分词处理(中文分词示例)
*/
private function tokenize($text) {
// 简单的分词,实际项目中建议使用 jieba-php 或 scws
$text = mb_strtolower($text, 'UTF-8');
// 去除标点符号和数字
$text = preg_replace('/[^\p{Han}\p{L}\s]/u', ' ', $text);
// 简单按空格分词
$words = preg_split('/\s+/', trim($text));
// 移除停用词
$stopWords = $this->getStopWords();
$words = array_filter($words, function($word) use ($stopWords) {
return !empty($word) && !in_array($word, $stopWords) && mb_strlen($word) > 1;
});
return array_values($words);
}
/**
* 处理文档
*/
private function processDocuments() {
$this->processedDocs = [];
$this->vocabulary = [];
foreach ($this->documents as $id => $document) {
$words = $this->tokenize($document);
$wordCount = array_count_values($words);
$this->processedDocs[$id] = [
'content' => $document,
'words' => $words,
'wordCount' => $wordCount,
'totalWords' => count($words)
];
// 构建词汇表
foreach ($wordCount as $word => $count) {
if (!isset($this->vocabulary[$word])) {
$this->vocabulary[$word] = 0;
}
$this->vocabulary[$word]++;
}
}
}
/**
* 计算TF值
*/
private function calculateTF($wordCount, $totalWords, $word) {
if (!isset($wordCount[$word]) || $totalWords == 0) {
return 0;
}
// TF = 词频 / 总词数
return $wordCount[$word] / $totalWords;
}
/**
* 计算IDF值
*/
private function calculateIDF($word) {
if (!isset($this->vocabulary[$word])) {
return 0;
}
if (isset($this->idfCache[$word])) {
return $this->idfCache[$word];
}
$totalDocs = count($this->documents);
$docsWithWord = $this->vocabulary[$word];
// IDF = log(总文档数 / (包含该词的文档数 + 1))
$idf = log(($totalDocs + 1) / ($docsWithWord + 1)) + 1;
$this->idfCache[$word] = $idf;
return $idf;
}
/**
* 计算文档的TF-IDF向量
*/
public function getDocumentVector($docId = null) {
$vector = [];
if ($docId !== null && isset($this->processedDocs[$docId])) {
$doc = $this->processedDocs[$docId];
$words = array_keys($this->vocabulary);
foreach ($words as $word) {
$tf = $this->calculateTF($doc['wordCount'], $doc['totalWords'], $word);
$idf = $this->calculateIDF($word);
$vector[$word] = $tf * $idf;
}
} else {
// 计算所有文档的向量
foreach ($this->processedDocs as $id => $doc) {
$words = array_keys($this->vocabulary);
$docVector = [];
foreach ($words as $word) {
$tf = $this->calculateTF($doc['wordCount'], $doc['totalWords'], $word);
$idf = $this->calculateIDF($word);
$docVector[$word] = $tf * $idf;
}
$vector[$id] = $docVector;
}
}
return $vector;
}
/**
* 获取文档中TF-IDF最高的关键词
*/
public function getKeywords($docId, $topN = 10) {
$vector = $this->getDocumentVector($docId);
if (empty($vector)) {
return [];
}
arsort($vector);
return array_slice($vector, 0, $topN, true);
}
/**
* 获取停用词列表
*/
private function getStopWords() {
return ['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这', '他', '她', '它', '们'];
}
}
文本相似度计算
余弦相似度实现
<?php
class TextSimilarity {
/**
* 计算余弦相似度
*/
public static function cosineSimilarity(array $vectorA, array $vectorB) {
$dotProduct = 0;
$normA = 0;
$normB = 0;
// 获取所有唯一的key
$allKeys = array_unique(array_merge(array_keys($vectorA), array_keys($vectorB)));
foreach ($allKeys as $key) {
$valueA = $vectorA[$key] ?? 0;
$valueB = $vectorB[$key] ?? 0;
$dotProduct += $valueA * $valueB;
$normA += $valueA * $valueA;
$normB += $valueB * $valueB;
}
$normA = sqrt($normA);
$normB = sqrt($normB);
if ($normA == 0 || $normB == 0) {
return 0;
}
return $dotProduct / ($normA * $normB);
}
/**
* 计算Jaccard相似度
*/
public static function jaccardSimilarity(array $setA, array $setB) {
$intersection = array_intersect($setA, $setB);
$union = array_unique(array_merge($setA, $setB));
if (empty($union)) {
return 0;
}
return count($intersection) / count($union);
}
/**
* 计算欧几里得距离
*/
public static function euclideanDistance(array $vectorA, array $vectorB) {
$distance = 0;
$allKeys = array_unique(array_merge(array_keys($vectorA), array_keys($vectorB)));
foreach ($allKeys as $key) {
$valueA = $vectorA[$key] ?? 0;
$valueB = $vectorB[$key] ?? 0;
$distance += pow($valueA - $valueB, 2);
}
return sqrt($distance);
}
/**
* 计算皮尔逊相关系数
*/
public static function pearsonCorrelation(array $vectorA, array $vectorB) {
$allKeys = array_unique(array_merge(array_keys($vectorA), array_keys($vectorB)));
$n = count($allKeys);
if ($n < 2) return 0;
$sumA = 0;
$sumB = 0;
$sumAB = 0;
$sumA2 = 0;
$sumB2 = 0;
foreach ($allKeys as $key) {
$valueA = $vectorA[$key] ?? 0;
$valueB = $vectorB[$key] ?? 0;
$sumA += $valueA;
$sumB += $valueB;
$sumAB += $valueA * $valueB;
$sumA2 += $valueA * $valueA;
$sumB2 += $valueB * $valueB;
}
$numerator = $n * $sumAB - $sumA * $sumB;
$denominator = sqrt(($n * $sumA2 - $sumA * $sumA) * ($n * $sumB2 - $sumB * $sumB));
if ($denominator == 0) {
return 0;
}
return $numerator / $denominator;
}
}
完整使用示例
<?php
require_once 'TFIDF.php';
require_once 'TextSimilarity.php';
// 创建文档集合
$documents = [
1 => "PHP是一种流行的服务器端脚本语言,广泛用于Web开发",
2 => "PHP语言非常适合Web开发,特别是动态网站开发",
3 => "Python也是一种流行的编程语言,用于数据分析和人工智能",
4 => "JavaScript是前端开发的主要语言,也用于后端开发"
];
// 初始化TF-IDF
$tfidf = new TFIDF($documents);
// 获取文档向量
$vector1 = $tfidf->getDocumentVector(1);
$vector2 = $tfidf->getDocumentVector(2);
// 计算相似度
$cosineSim = TextSimilarity::cosineSimilarity($vector1, $vector2);
echo "文档1和文档2的余弦相似度: " . round($cosineSim, 4) . "\n";
// 获取关键词
$keywords1 = $tfidf->getKeywords(1, 5);
echo "\n文档1的关键词:\n";
foreach ($keywords1 as $word => $score) {
echo "$word: " . round($score, 4) . "\n";
}
// 比较所有文档
echo "\n所有文档相似度矩阵:\n";
$totalDocs = count($documents);
for ($i = 1; $i <= $totalDocs; $i++) {
for ($j = $i + 1; $j <= $totalDocs; $j++) {
$vecI = $tfidf->getDocumentVector($i);
$vecJ = $tfidf->getDocumentVector($j);
$sim = TextSimilarity::cosineSimilarity($vecI, $vecJ);
echo "文档{$i} - 文档{$j}: " . round($sim, 4) . "\n";
}
}
// 搜索相关文档
$searchQuery = "Web开发语言";
$searchResults = searchDocuments($tfidf, $documents, $searchQuery, $tfidf);
echo "\n搜索 '{$searchQuery}' 的结果:\n";
foreach ($searchResults as $docId => $score) {
echo "文档{$docId}: " . round($score, 4) . " - " . mb_substr($documents[$docId], 0, 30) . "...\n";
}
/**
* 搜索文档
*/
function searchDocuments($tfidf, $documents, $query, $existingTfidf) {
// 创建临时文档集处理查询
$allDocs = $documents;
$allDocs['query'] = $query;
$tempTfidf = new TFIDF($allDocs);
$queryVector = $tempTfidf->getDocumentVector('query');
$results = [];
foreach ($documents as $id => $content) {
$docVector = $tempTfidf->getDocumentVector($id);
$results[$id] = TextSimilarity::cosineSimilarity($queryVector, $docVector);
}
arsort($results);
return $results;
}
性能优化建议
<?php
class OptimizedTFIDF extends TFIDF {
/**
* 使用字典树优化词汇表
*/
private $trie = [];
/**
* 缓存已计算的向量
*/
private $vectorCache = [];
/**
* 批量处理文档
*/
public function batchProcess(array $documents) {
$batches = array_chunk($documents, 100); // 每批处理100个文档
foreach ($batches as $batch) {
$this->processBatch($batch);
}
}
/**
* 增量更新
*/
public function addDocument($id, $content) {
$this->documents[$id] = $content;
$words = $this->tokenize($content);
$wordCount = array_count_values($words);
$this->processedDocs[$id] = [
'content' => $content,
'words' => $words,
'wordCount' => $wordCount,
'totalWords' => count($words)
];
// 更新词汇表
foreach ($wordCount as $word => $count) {
if (!isset($this->vocabulary[$word])) {
$this->vocabulary[$word] = 0;
}
$this->vocabulary[$word]++;
}
// 清除缓存
$this->vectorCache = [];
$this->idfCache = [];
}
}
注意事项
中文分词
- 需要接入专业分词库(如 scws, jieba-php)
- 处理新词识别和歧义消除
性能考虑
- 文档量较大时考虑使用缓存
- 可以使用内存数据库(Redis)存储TF-IDF向量
- 大规模文档建议使用搜索引擎(如Elasticsearch)
准确度优化
- 选择合适的停用词表
- 考虑词性过滤
- 可以对不同词性赋予不同权重
这个实现提供了完整的TF-IDF和文本相似度计算功能,可以根据实际需求进行调整和优化。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播