PHP项目TF-IDF与相似度

wen PHP项目 2

本文目录导读:

PHP项目TF-IDF与相似度

  1. TF-IDF基本概念
  2. PHP实现TF-IDF
  3. 文本相似度计算
  4. 完整使用示例
  5. 性能优化建议
  6. 注意事项

我来详细介绍如何在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和文本相似度计算功能,可以根据实际需求进行调整和优化。

抱歉,评论功能暂时关闭!