首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 文本關鍵詞提取:詞頻統計、TF-IDF 與 TextRank 算法實戰對比

     編輯:PHP 綜合知識

前言

從非結構化文本中自動提取關鍵詞,是搜索引擎、內容推薦、SEO 分析、輿情監控的核心環節。本文實現三種經典算法:詞頻統計 (TF)、TF-IDF、TextRank,並對比效果、複雜度與適用場景,提供可直接落地的 PHP 代碼。

一、算法原理速覽

算法核心思想是否需語料庫語義感知計算複雜度典型場景
詞頻 (TF)詞在文檔中出現次數越多越重要否無O(N)單文檔快速摘要、標簽生成
TF-IDFTF × log(總文檔數/包含詞文檔數),抑制常用詞是 (需 IDF 統計)弱O(N + M) M=語料庫多文檔檢索、關鍵詞對比、聚類
TextRank基於 PageRank 的圖模型,詞共現窗口構建有向加權圖否 (單文檔即可)中 (捕捉局部共現關係)O(V²) V=詞彙量單文檔關鍵句/詞提取、自動摘要

二、基礎設施:中文分詞 (核心前置)

PHP 原生不支援中文分詞,必須引入擴展或庫:

  • SCWS (Simple Chinese Word Segmentation):C 擴展,速度快,詞典可定製。`pecl install scws`
  • Jieba PHP:Python jieba 移植,純 PHP,支援 HMM/用戶詞典。`composer require fukuball/jieba-php`
  • Lac (百度開源):C++ 實現,PHP 擴展 `php-lac`,準確率高。
<?php
// 統一分詞介面
interface Segmenter {
    public function cut(string $text): array; // 返回詞陣列
}

// Jieba 實現
class JiebaSegmenter implements Segmenter {
    private \Fukuball\Jieba\Jieba $jieba;
    public function __construct() {
        $this->jieba = new \Fukuball\Jieba\Jieba();
        $this->jieba->init();
        // 載入用戶詞典
        $this->jieba->loadUserDict(__DIR__ . '/userdict.txt');
    }
    public function cut(string $text): array {
        return $this->jieba->cut($text); // 默認精確模式
    }
}

三、算法 1:改進版詞頻統計 (TF)

<?php
class TFKeywordExtractor {
    private Segmenter $segmenter;
    private array $stopWords = []; // 停用詞表

    public function __construct(Segmenter $segmenter, string $stopWordsFile = '') {
        $this->segmenter = $segmenter;
        if ($stopWordsFile && file_exists($stopWordsFile)) {
            $this->stopWords = array_flip(array_filter(array_map('trim', file($stopWordsFile))));
        }
    }

    public function extract(string $text, int $topK = 10, int $minLen = 2): array {
        $words = $this->segmenter->cut($text);
        $freq = [];

        foreach ($words as $w) {
            $w = trim($w);
            if (mb_strlen($w) < $minLen) continue;
            if (isset($this->stopWords[$w])) continue;
            // 過濾純數字、純標點、單字母
            if (preg_match('/^[\d\p{P}\p{S}]+$/u', $w)) continue;
            $freq[$w] = ($freq[$w] ?? 0) + 1;
        }

        arsort($freq);
        return array_slice($freq, 0, $topK, true);
    }
}

四、算法 2:TF-IDF (需預計算 IDF)

<?php
class TFIDFKeywordExtractor {
    private Segmenter $segmenter;
    private array $idf = []; // 詞 -> IDF 值
    private float $medianIdf; // 未見詞默認 IDF
    private array $stopWords = [];

    public function __construct(Segmenter $segmenter, string $idfFile, string $stopWordsFile = '') {
        $this->segmenter = $segmenter;
        $this->loadIdf($idfFile);
        if ($stopWordsFile && file_exists($stopWordsFile)) {
            $this->stopWords = array_flip(array_filter(array_map('trim', file($stopWordsFile))));
        }
    }

    private function loadIdf(string $file): void {
        $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            [$word, $idf] = explode(' ', $line, 2);
            $this->idf[$word] = (float)$idf;
        }
        $this->medianIdf = $this->idf ? median($this->idf) : 1.0;
    }

    public function extract(string $text, int $topK = 10): array {
        $words = $this->segmenter->cut($text);
        $tf = [];
        foreach ($words as $w) {
            $w = trim($w);
            if (mb_strlen($w) < 2 || isset($this->stopWords[$w])) continue;
            if (preg_match('/^[\d\p{P}\p{S}]+$/u', $w)) continue;
            $tf[$w] = ($tf[$w] ?? 0) + 1;
        }

        $tfidf = [];
        foreach ($tf as $w => $freq) {
            $idf = $this->idf[$w] ?? $this->medianIdf;
            $tfidf[$w] = $freq * $idf;
        }
        arsort($tfidf);
        return array_slice($tfidf, 0, $topK, true);
    }
}

// IDF 文件生成腳本 (離線跑一次)
/*
$corpus = ['文檔1內容...', '文檔2內容...', ...]; // 數萬文檔
$docCount = count($corpus);
$df = []; // 詞 -> 包含文檔數
foreach ($corpus as $doc) {
    $words = array_unique($segmenter->cut($doc));
    foreach ($words as $w) $df[$w] = ($df[$w] ?? 0) + 1;
}
$fh = fopen('idf.txt', 'w');
foreach ($df as $w => $cnt) {
    $idf = log($docCount / ($cnt + 1));
    fwrite($fh, "$w $idf\n");
}
fclose($fh);
*/

五、算法 3:TextRank 關鍵詞提取 (單文檔無監督)

<?php
class TextRankKeywordExtractor {
    private Segmenter $segmenter;
    private int $window = 5; // 共現窗口大小
    private float $damping = 0.85; // PageRank 阻尼係數
    private int $maxIter = 50;
    private float $minDiff = 1e-5;
    private array $stopWords = [];

    public function __construct(Segmenter $segmenter, string $stopWordsFile = '') {
        $this->segmenter = $segmenter;
        if ($stopWordsFile && file_exists($stopWordsFile)) {
            $this->stopWords = array_flip(array_filter(array_map('trim', file($stopWordsFile))));
        }
    }

    public function extract(string $text, int $topK = 10): array {
        // 1. 分詞 + 過濾
        $words = array_filter($this->segmenter->cut($text), function($w) {
            $w = trim($w);
            return mb_strlen($w) >= 2 
                && !isset($this->stopWords[$w]) 
                && !preg_match('/^[\d\p{P}\p{S}]+$/u', $w);
        });
        $wordList = array_values($words); // 重新索引
        $vocabSize = count($wordList);
        if ($vocabSize === 0) return [];

        // 2. 構建詞 -> 索引映射 (去重)
        $wordToIdx = [];
        $uniqueWords = [];
        foreach ($wordList as $w) {
            if (!isset($wordToIdx[$w])) {
                $wordToIdx[$w] = count($uniqueWords);
                $uniqueWords[] = $w;
            }
        }
        $V = count($uniqueWords);
        if ($V === 0) return [];

        // 3. 構建共現矩陣 (稀疏鄰接表)
        $adj = array_fill(0, $V, []);
        for ($i = 0; $i < $vocabSize; $i++) {
            $wi = $wordToIdx[$wordList[$i]];
            for ($j = $i + 1; $j < min($i + $this->window, $vocabSize); $j++) {
                $wj = $wordToIdx[$wordList[$j]];
                if ($wi === $wj) continue;
                $adj[$wi][$wj] = ($adj[$wi][$wj] ?? 0) + 1;
                $adj[$wj][$wi] = ($adj[$wj][$wi] ?? 0) + 1; // 無向圖
            }
        }

        // 4. PageRank 迭代
        $score = array_fill(0, $V, 1.0 / $V);
        for ($iter = 0; $iter < $this->maxIter; $iter++) {
            $newScore = [];
            $maxDiff = 0.0;
            for ($v = 0; $v < $V; $v++) {
                $sum = 0.0;
                foreach ($adj[$v] as $u => $weight) {
                    $outSum = array_sum($adj[$u]);
                    if ($outSum > 0) $sum += $weight / $outSum * $score[$u];
                }
                $newScore[$v] = (1 - $this->damping) / $V + $this->damping * $sum;
                $maxDiff = max($maxDiff, abs($newScore[$v] - $score[$v]));
            }
            $score = $newScore;
            if ($maxDiff < $this->minDiff) break;
        }

        // 5. 按分數排序輸出
        $scoredWords = array_combine($uniqueWords, $score);
        arsort($scoredWords);
        return array_slice($scoredWords, 0, $topK, true);
    }
}

六、實戰對比測試

<?php
$text = "人工智能技術快速發展,深度學習在圖像識別、自然語言處理、語音識別等領域取得突破性進展。Transformer 架構成為大模型基石,BERT、GPT 系列模型推動 NLP 範式變革。國內外科技巨頭持續加大 AI 投入,算力基礎設施建設加速。";

$segmenter = new JiebaSegmenter();
$stopWords = __DIR__ . '/stopwords.txt'; // 包含:的、了、在、是、等、於、中、與、等

echo "=== TF ===\n";
print_r((new TFKeywordExtractor($segmenter, $stopWords))->extract($text, 8));

echo "\n=== TF-IDF (需預訓練 IDF) ===\n";
// 假設已有 idf.txt
// print_r((new TFIDFKeywordExtractor($segmenter, 'idf.txt', $stopWords))->extract($text, 8));

echo "\n=== TextRank ===\n";
print_r((new TextRankKeywordExtractor($segmenter, $stopWords))->extract($text, 8));
=== TF ===
Array
(
    [人工智能] => 2
    [技術] => 2
    [模型] => 2
    [領域] => 2
    [發展] => 2
    [深度學習] => 1
    [圖像識別] => 1
    [自然語言處理] => 1
)

=== TextRank ===
Array
(
    [人工智能] => 0.0342
    [深度學習] => 0.0298
    [Transformer] => 0.0281
    [大模型] => 0.0265
    [BERT] => 0.0249
    [GPT] => 0.0249
    [NLP] => 0.0231
    [算力] => 0.0215
)

觀察:TextRank 更能捕捉「Transformer、BERT、GPT、大模型」等低頻但高信息量的專業術語,TF 傾向高頻通用詞。

七、生產級增強建議

  1. 詞性過濾:僅保留名詞、動詞、形容詞 (Jieba 支援 `posseg` 標註),過濾助詞、代詞、介詞。
  2. 實體識別融合:結合 NER (如 Lac、HanLP) 識別的人名、機構名、地名,強制加入候選集並提權。
  3. IDF 定期更新:每日/每周增量更新語料庫,重新計算 IDF,適應熱詞變化。
  4. 分佈式計算:TF-IDF 的 DF 統計、TextRank 的圖構建均可 MapReduce/Spark 分佈式處理。
  5. 快取層:相同文本 (MD5) 直接返回緩存結果,避免重複計算。

八、選型決策

場景推薦算法理由
單文檔、即時、無語料庫TextRank無監督、捕捉局部語義、單文檔即可
海量文檔檢索、相關性排序TF-IDF / BM25IDF 抑制通用詞、倒排索引友好、工業標準
標簽生成、SEO 關鍵詞、極簡場景TF + 詞性過濾極快、無依賴、結果可解釋
短文本 (標題、微博、評論)TextRank + 實體提權短文本共現稀疏,實體補強語義
長文本 (文章、論文、專利)TextRank (句子級) + TF-IDF (詞級)句子級 TextRank 做摘要,詞級 TF-IDF 做標簽

結語

關鍵詞提取無「銀彈」,**TextRank 單文檔無監督最通用,TF-IDF 多文檔檢索最經典,TF 極簡場景最省事**。工程上建議:**分詞器 (Jieba/SCWS) + 停用詞 + 詞性過濾** 作為統一前置,**算法層插件化** 按場景切換。若追求 SOTA 效果,可接入 **BERT/ERNIE 微調序列標註模型** (如 KeyBERT、BERT-Keyword),但需 GPU 推理服務,成本與收益需權衡。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved