怎样在PHP项目中实现余弦相似度?

wen java案例 3

本文目录导读:

怎样在PHP项目中实现余弦相似度?

  1. 基础余弦相似度函数
  2. 文本向量化实现
  3. 完整使用示例
  4. 性能优化建议
  5. 高级实现:使用外部库
  6. 注意事项

在PHP中实现余弦相似度主要涉及文本向量化和数学计算两个核心步骤,以下是完整的实现方案:

基础余弦相似度函数

<?php
/**
 * 计算两个向量的余弦相似度
 * @param array $vectorA 向量A
 * @param array $vectorB 向量B
 * @return float 余弦相似度 (-1到1)
 */
function cosineSimilarity(array $vectorA, array $vectorB): float {
    // 确保两个向量长度相同
    if (count($vectorA) !== count($vectorB)) {
        throw new InvalidArgumentException('向量维度必须相同');
    }
    $dotProduct = 0;
    $normA = 0;
    $normB = 0;
    foreach ($vectorA as $i => $valueA) {
        $valueB = $vectorB[$i];
        $dotProduct += $valueA * $valueB;
        $normA += $valueA * $valueA;
        $normB += $valueB * $valueB;
    }
    $normA = sqrt($normA);
    $normB = sqrt($normB);
    // 防止除以0
    if ($normA == 0 || $normB == 0) {
        return 0.0;
    }
    return $dotProduct / ($normA * $normB);
}

文本向量化实现

1 简单词频向量化

<?php
/**
 * 将文本转为词频向量
 * @param string $text 输入文本
 * @param array $vocabulary 词汇表(可选)
 * @return array 词频向量
 */
function textToVector(string $text, array $vocabulary = []): array {
    // 分词(简单实现,可根据需求改进)
    $words = preg_split('/\s+/', mb_strtolower($text));
    // 去除标点符号
    $words = array_map(function($word) {
        return preg_replace('/[^\w\s]/u', '', $word);
    }, $words);
    // 过滤空词
    $words = array_filter($words);
    // 统计词频
    $wordFreq = array_count_values($words);
    // 如果没有提供词汇表,使用文本中的词构建
    if (empty($vocabulary)) {
        $vocabulary = array_keys($wordFreq);
    }
    // 构建向量
    $vector = [];
    foreach ($vocabulary as $word) {
        $vector[] = $wordFreq[$word] ?? 0;
    }
    return $vector;
}
/**
 * 计算两段文本的余弦相似度
 */
function textCosineSimilarity(string $textA, string $textB): float {
    // 构建联合词汇表
    $wordsA = array_keys(array_count_values(
        array_filter(preg_split('/\s+/', mb_strtolower($textA)))
    ));
    $wordsB = array_keys(array_count_values(
        array_filter(preg_split('/\s+/', mb_strtolower($textB)))
    ));
    $vocabulary = array_unique(array_merge($wordsA, $wordsB));
    // 向量化
    $vectorA = textToVector($textA, $vocabulary);
    $vectorB = textToVector($textB, $vocabulary);
    return cosineSimilarity($vectorA, $vectorB);
}

2 TF-IDF向量化(更精确)

<?php
class TFIDFSimilarity {
    private array $documents;
    private array $vocabulary = [];
    private array $idfValues = [];
    public function __construct(array $documents) {
        $this->documents = $documents;
        $this->buildVocabulary();
        $this->calculateIDF();
    }
    private function buildVocabulary(): void {
        $allWords = [];
        foreach ($this->documents as $doc) {
            $words = $this->tokenize($doc);
            $allWords = array_merge($allWords, $words);
        }
        $this->vocabulary = array_unique($allWords);
    }
    private function tokenize(string $text): array {
        $text = mb_strtolower($text);
        $words = preg_split('/\s+/', $text);
        return array_filter($words, function($word) {
            return !empty(trim($word));
        });
    }
    private function calculateIDF(): void {
        $docCount = count($this->documents);
        foreach ($this->vocabulary as $word) {
            $docWithWord = 0;
            foreach ($this->documents as $doc) {
                $words = $this->tokenize($doc);
                if (in_array($word, $words)) {
                    $docWithWord++;
                }
            }
            // IDF = log(文档总数 / 包含该词的文档数)
            $this->idfValues[$word] = log(($docCount + 1) / ($docWithWord + 1)) + 1;
        }
    }
    public function textToTfidfVector(string $text): array {
        $words = $this->tokenize($text);
        $wordCount = count($words);
        $vector = [];
        foreach ($this->vocabulary as $word) {
            $tf = empty($words) ? 0 : array_count_values($words)[$word] ?? 0;
            $tf = $tf / $wordCount; // 归一化词频
            $idf = $this->idfValues[$word] ?? 0;
            $vector[] = $tf * $idf;
        }
        return $vector;
    }
    public function calculateSimilarity(string $textA, string $textB): float {
        $vectorA = $this->textToTfidfVector($textA);
        $vectorB = $this->textToTfidfVector($textB);
        return cosineSimilarity($vectorA, $vectorB);
    }
}

完整使用示例

<?php
// 示例1:简单文本相似度比较
$text1 = "机器学习和深度学习是人工智能的核心技术";
$text2 = "深度学习在自然语言处理中有广泛应用";
$similarity = textCosineSimilarity($text1, $text2);
echo "文本相似度: " . round($similarity, 4) . PHP_EOL;
// 示例2:使用TF-IDF
$documents = [
    "人工智能可以模拟人类智能",
    "机器学习是人工智能的重要分支",
    "深度学习让机器学习更加强大",
    "自然语言处理需要深度学习技术",
    "Python是机器学习常用的编程语言"
];
$tfidf = new TFIDFSimilarity($documents);
$query1 = "人工智能机器学习";
$query2 = "深度学习自然语言";
$similarity = $tfidf->calculateSimilarity($query1, $query2);
echo "TF-IDF相似度: " . round($similarity, 4) . PHP_EOL;
// 示例3:文档搜索
function searchSimilarDocuments(string $query, array $documents, TFIDFSimilarity $tfidf): array {
    $results = [];
    foreach ($documents as $index => $doc) {
        $similarity = $tfidf->calculateSimilarity($query, $doc);
        $results[] = [
            'index' => $index,
            'document' => $doc,
            'similarity' => $similarity
        ];
    }
    usort($results, function($a, $b) {
        return $b['similarity'] <=> $a['similarity'];
    });
    return $results;
}
$query = "人工智能技术";
$results = searchSimilarDocuments($query, $documents, $tfidf);
echo "搜索\"$query\"的结果:" . PHP_EOL;
foreach (array_slice($results, 0, 3) as $result) {
    echo sprintf("相似度 %.4f: %s\n", 
        $result['similarity'], 
        $result['document']
    );
}

性能优化建议

<?php
// 1. 使用缓存存储向量
class CachedVectorSimilarity {
    private array $vectorCache = [];
    private TFIDFSimilarity $tfidf;
    public function __construct(TFIDFSimilarity $tfidf) {
        $this->tfidf = $tfidf;
    }
    public function getVector(string $text): array {
        $key = md5($text);
        if (!isset($this->vectorCache[$key])) {
            $this->vectorCache[$key] = $this->tfidf->textToTfidfVector($text);
        }
        return $this->vectorCache[$key];
    }
}
// 2. 使用SplFixedArray提高性能
function optimizedCosineSimilarity(array $vectorA, array $vectorB): float {
    $a = SplFixedArray::fromArray($vectorA);
    $b = SplFixedArray::fromArray($vectorB);
    $dotProduct = 0;
    $normA = 0;
    $normB = 0;
    for ($i = 0; $i < $a->getSize(); $i++) {
        $dotProduct += $a[$i] * $b[$i];
        $normA += $a[$i] * $a[$i];
        $normB += $b[$i] * $b[$i];
    }
    return $dotProduct / (sqrt($normA) * sqrt($normB));
}

高级实现:使用外部库

# 使用composer安装数学库
composer require markrogoyski/math-php
<?php
use MathPHP\LinearAlgebra\Vector;
function cosineSimilarityWithLibrary(array $vectorA, array $vectorB): float {
    $v1 = new Vector($vectorA);
    $v2 = new Vector($vectorB);
    return $v1->dotProduct($v2) / ($v1->l2Norm() * $v2->l2Norm());
}

注意事项

  1. 中文分词:中文文本需要先分词,建议使用 jieba-phpscws 等分词库
  2. 停用词处理:过滤常见的无意义词(的、了、是等)
  3. 向量维度:词汇表大小直接影响性能和内存使用
  4. 精度问题:使用 float 类型时注意精度损失

这个实现提供了从基础到高级的多种方案,可以根据具体需求选择合适的实现方式。

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