HarmonyOS APP开发:关键词提取与标签生成

举报
Jack20 发表于 2026/06/21 14:01:17 2026/06/21
【摘要】 HarmonyOS APP开发:关键词提取与标签生成核心要点:本文深入讲解HarmonyOS平台上的关键词提取与标签生成技术,涵盖TF-IDF算法实现、TextRank图排序算法、基于语义的标签聚类、以及标签推荐与管理的完整工程实践。 一、背景与动机你有没有想过,为什么今日头条能精准推送你感兴趣的内容?为什么小红书的标签系统那么好用?为什么你的笔记APP搜东西总是找不到?答案就藏在"关键词...

HarmonyOS APP开发:关键词提取与标签生成

核心要点:本文深入讲解HarmonyOS平台上的关键词提取与标签生成技术,涵盖TF-IDF算法实现、TextRank图排序算法、基于语义的标签聚类、以及标签推荐与管理的完整工程实践。


一、背景与动机

你有没有想过,为什么今日头条能精准推送你感兴趣的内容?为什么小红书的标签系统那么好用?为什么你的笔记APP搜东西总是找不到?

答案就藏在"关键词提取"和"标签生成"这两个技术里。

关键词提取,是从一段文本中自动找出最能代表其核心内容的词或短语。这就像给一篇文章写摘要——不是逐字逐句地复述,而是提炼出最关键的信息点。标签生成则更进一步,它不仅提取关键词,还会对这些关键词进行归类、去重、扩展,最终生成一套结构化的标签体系。

在HarmonyOS APP开发中,关键词提取和标签生成的应用场景非常广泛:

  • 笔记/文档管理:自动为笔记打标签,方便分类和检索
  • 内容推荐:根据文章关键词匹配用户兴趣标签
  • 搜索优化:提取关键词建立倒排索引,提升搜索效率
  • 知识图谱:从文本中提取实体和关系,构建知识网络

今天我们就来深入探讨,如何在HarmonyOS端侧实现一套高效的关键词提取与标签生成系统。


二、核心原理

2.1 关键词提取算法对比

目前主流的关键词提取算法有三类:

算法 原理 优点 缺点
TF-IDF 词频×逆文档频率 简单高效,无需训练 忽略词序和语义
TextRank 基于图的排序算法 考虑词间关系,无需语料 计算量较大
语义模型 深度学习语义理解 准确率高,支持同义词 需要模型和算力

在端侧场景下,TF-IDF和TextRank是更实用的选择——它们不需要模型文件,计算开销可控,而且效果已经足够好。

2.2 TF-IDF算法详解

TF-IDF的核心思想很简单:一个词在一篇文章中出现次数越多(TF高),同时在所有文章中出现越少(IDF高),这个词就越重要。

  • TF(Term Frequency):词频,某个词在文档中出现的频率
  • IDF(Inverse Document Frequency):逆文档频率,衡量一个词的"普遍重要性"
TF(t,d) = 词t在文档d中出现的次数 / 文档d的总词数
IDF(t) = log(总文档数 / 包含词t的文档数)
TF-IDF(t,d) = TF(t,d) × IDF(t)

2.3 TextRank算法详解

TextRank的思想来自PageRank——如果很多重要的词都和某个词相邻,那这个词也很重要。
图片.png

TextRank的核心公式:

WS(Vi) = (1-d) + d × Σ(Vj∈In(Vi)) [ wji / Σ(Vk∈Out(Vj)) wjk ] × WS(Vj)

其中 d 是阻尼系数(通常取0.85),wji 是词Vj到Vi的边权重。

2.4 标签生成的完整流程

关键词提取只是第一步,标签生成还需要:

  1. 关键词清洗:去除停用词、低频词、无意义词
  2. 语义聚类:将语义相近的关键词合并为一个标签
  3. 标签扩展:基于同义词词典和上下位关系扩展标签
  4. 标签排序:根据重要性和多样性排序
  5. 标签去重:去除语义重复的标签

三、代码实战

3.1 TF-IDF关键词提取器

/**
 * TF-IDF关键词提取器
 * 支持增量计算IDF,适合持续更新的文档集合
 */

// 关键词结果
interface KeywordResult {
  word: string;          // 关键词
  score: number;         // TF-IDF分数
  tf: number;            // 词频
  idf: number;           // 逆文档频率
  rank: number;          // 排名
}

// 文档记录
interface DocumentRecord {
  id: string;
  wordFreq: Map<string, number>;  // 词频统计
  totalWords: number;              // 总词数
}

export class TFIDFExtractor {
  // 文档集合
  private documents: Map<string, DocumentRecord> = new Map();
  // 全局词文档频率(包含某词的文档数)
  private docFrequency: Map<string, number> = new Map();
  // 停用词表
  private stopWords: Set<string>;
  // IDF缓存
  private idfCache: Map<string, number> = new Map();
  private idfCacheValid: boolean = false;

  constructor() {
    this.stopWords = this.initStopWords();
  }

  // 初始化中文停用词
  private initStopWords(): Set<string> {
    const words = [
      '的', '了', '在', '是', '我', '有', '和', '就', '不', '人',
      '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去',
      '你', '会', '着', '没有', '看', '好', '自己', '这', '他', '她',
      '什么', '那', '又', '被', '从', '它', '把', '对', '而', '但',
      '与', '或', '如果', '因为', '所以', '虽然', '但是', '可以',
      '这个', '那个', '这些', '那些', '之', '其', '以', '及',
      '等', '吗', '吧', '呢', '啊', '哦', '嗯', '哈', '呀',
    ];
    return new Set(words);
  }

  // 简易中文分词(基于规则,实际项目应使用NLP分词API)
  private tokenize(text: string): string[] {
    const tokens: string[] = [];
    const chars = [...text];

    let i = 0;
    while (i < chars.length) {
      // 跳过空白和标点
      if (/[\s,。!?、;:""''()【】《》\-,.\!?;:'"()\[\]{}<>]/.test(chars[i])) {
        i++;
        continue;
      }

      // 尝试匹配2-4字词(贪心匹配)
      let matched = false;
      for (let len = 4; len >= 2; len--) {
        if (i + len > chars.length) continue;
        const word = chars.slice(i, i + len).join('');

        // 简单的词性判断:连续中文字符
        if (/^[\u4e00-\u9fa5]+$/.test(word) && !this.stopWords.has(word)) {
          tokens.push(word);
          i += len;
          matched = true;
          break;
        }
      }

      if (!matched) {
        // 单字也保留(可能是专有名词的一部分)
        if (/[\u4e00-\u9fa5]/.test(chars[i]) && !this.stopWords.has(chars[i])) {
          tokens.push(chars[i]);
        }
        i++;
      }
    }

    return tokens;
  }

  // 添加文档到集合
  addDocument(docId: string, text: string): void {
    const tokens = this.tokenize(text);
    const wordFreq = new Map<string, number>();

    tokens.forEach(word => {
      wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
    });

    // 更新文档频率
    const uniqueWords = new Set(tokens);
    uniqueWords.forEach(word => {
      this.docFrequency.set(word, (this.docFrequency.get(word) || 0) + 1);
    });

    this.documents.set(docId, {
      id: docId,
      wordFreq,
      totalWords: tokens.length,
    });

    // IDF缓存失效
    this.idfCacheValid = false;
  }

  // 计算IDF值
  private getIDF(word: string): number {
    if (this.idfCacheValid && this.idfCache.has(word)) {
      return this.idfCache.get(word)!;
    }

    const df = this.docFrequency.get(word) || 0;
    const totalDocs = this.documents.size;

    // 使用平滑IDF,避免除零
    const idf = Math.log((totalDocs + 1) / (df + 1)) + 1;
    this.idfCache.set(word, idf);

    return idf;
  }

  // 重建IDF缓存
  private rebuildIDFCache(): void {
    if (this.idfCacheValid) return;
    this.idfCache.clear();

    const allWords = new Set<string>();
    this.docFrequency.forEach((_, word) => allWords.add(word));

    allWords.forEach(word => {
      this.idfCache.set(word, this.getIDF(word));
    });

    this.idfCacheValid = true;
  }

  // 从指定文档提取关键词
  extractKeywords(docId: string, topN: number = 10): KeywordResult[] {
    const doc = this.documents.get(docId);
    if (!doc) return [];

    this.rebuildIDFCache();

    const results: KeywordResult[] = [];

    doc.wordFreq.forEach((freq, word) => {
      // 计算TF
      const tf = freq / doc.totalWords;
      // 计算IDF
      const idf = this.getIDF(word);
      // TF-IDF分数
      const score = tf * idf;

      results.push({ word, score, tf, idf, rank: 0 });
    });

    // 按分数降序排序
    results.sort((a, b) => b.score - a.score);

    // 设置排名并截取TopN
    return results.slice(0, topN).map((r, idx) => ({ ...r, rank: idx + 1 }));
  }

  // 从新文本提取关键词(不添加到文档集合)
  extractFromText(text: string, topN: number = 10): KeywordResult[] {
    const tokens = this.tokenize(text);
    const wordFreq = new Map<string, number>();

    tokens.forEach(word => {
      wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
    });

    this.rebuildIDFCache();

    const totalWords = tokens.length;
    const results: KeywordResult[] = [];

    wordFreq.forEach((freq, word) => {
      const tf = freq / totalWords;
      const idf = this.idfCache.get(word) || Math.log(this.documents.size + 1) + 1;
      const score = tf * idf;

      results.push({ word, score, tf, idf, rank: 0 });
    });

    results.sort((a, b) => b.score - a.score);
    return results.slice(0, topN).map((r, idx) => ({ ...r, rank: idx + 1 }));
  }

  // 获取文档数量
  getDocumentCount(): number {
    return this.documents.size;
  }

  // 清除所有文档
  clear(): void {
    this.documents.clear();
    this.docFrequency.clear();
    this.idfCache.clear();
    this.idfCacheValid = false;
  }
}

3.2 TextRank关键词提取器

/**
 * TextRank关键词提取器
 * 基于图排序的算法,考虑词间共现关系
 */

// 图节点
interface GraphNode {
  word: string;
  score: number;
  edges: Map<string, number>;  // 邻接表:目标词 → 权重
}

// TextRank配置
interface TextRankConfig {
  windowSize: number;     // 共现窗口大小
  dampingFactor: number;  // 阻尼系数
  maxIterations: number;  // 最大迭代次数
  convergenceThreshold: number;  // 收敛阈值
  minWordLength: number;  // 最小词长
}

export class TextRankExtractor {
  private config: TextRankConfig;
  private stopWords: Set<string>;

  constructor(config?: Partial<TextRankConfig>) {
    this.config = {
      windowSize: 5,
      dampingFactor: 0.85,
      maxIterations: 100,
      convergenceThreshold: 0.0001,
      minWordLength: 2,
      ...config,
    };
    this.stopWords = this.initStopWords();
  }

  // 初始化停用词
  private initStopWords(): Set<string> {
    return new Set([
      '的', '了', '在', '是', '我', '有', '和', '就', '不', '人',
      '都', '一', '上', '也', '很', '到', '说', '要', '去', '你',
      '会', '着', '没有', '看', '好', '自己', '这', '他', '她',
      '什么', '那', '又', '被', '从', '把', '对', '而', '但',
    ]);
  }

  // 简易分词
  private tokenize(text: string): string[] {
    const tokens: string[] = [];
    const chars = [...text];
    let i = 0;

    while (i < chars.length) {
      if (/[\s,。!?、;:""''()【】《》\-,.\!?;:'"()\[\]{}<>]/.test(chars[i])) {
        i++;
        continue;
      }

      // 尝试2-4字匹配
      let matched = false;
      for (let len = 4; len >= 2; len--) {
        if (i + len > chars.length) continue;
        const word = chars.slice(i, i + len).join('');
        if (/^[\u4e00-\u9fa5]+$/.test(word) && !this.stopWords.has(word)) {
          tokens.push(word);
          i += len;
          matched = true;
          break;
        }
      }

      if (!matched) {
        if (/[\u4e00-\u9fa5]/.test(chars[i]) && !this.stopWords.has(chars[i])) {
          tokens.push(chars[i]);
        }
        i++;
      }
    }

    return tokens;
  }

  // 构建词图
  private buildGraph(tokens: string[]): Map<string, GraphNode> {
    const graph = new Map<string, GraphNode>();

    // 过滤短词
    const filteredTokens = tokens.filter(t => t.length >= this.config.minWordLength);

    // 初始化节点
    filteredTokens.forEach(word => {
      if (!graph.has(word)) {
        graph.set(word, {
          word,
          score: 1.0,  // 初始权重
          edges: new Map(),
        });
      }
    });

    // 构建边(基于共现窗口)
    for (let i = 0; i < filteredTokens.length; i++) {
      const currentWord = filteredTokens[i];

      for (let j = i + 1; j < Math.min(i + this.config.windowSize, filteredTokens.length); j++) {
        const neighborWord = filteredTokens[j];

        if (currentWord !== neighborWord) {
          const node = graph.get(currentWord)!;
          const weight = (node.edges.get(neighborWord) || 0) + 1;
          node.edges.set(neighborWord, weight);

          // 无向图,双向添加
          const neighborNode = graph.get(neighborWord)!;
          const reverseWeight = (neighborNode.edges.get(currentWord) || 0) + 1;
          neighborNode.edges.set(currentWord, reverseWeight);
        }
      }
    }

    return graph;
  }

  // 迭代计算TextRank值
  private runTextRank(graph: Map<string, GraphNode>): void {
    const d = this.config.dampingFactor;

    for (let iter = 0; iter < this.config.maxIterations; iter++) {
      let maxDiff = 0;

      graph.forEach((node, word) => {
        let rankSum = 0;

        // 遍历所有指向当前节点的邻居
        node.edges.forEach((weight, neighborWord) => {
          const neighbor = graph.get(neighborWord);
          if (neighbor) {
            // 计算邻居的出边权重总和
            let outWeightSum = 0;
            neighbor.edges.forEach(w => { outWeightSum += w; });

            if (outWeightSum > 0) {
              rankSum += (weight / outWeightSum) * neighbor.score;
            }
          }
        });

        // TextRank公式
        const newScore = (1 - d) + d * rankSum;
        const diff = Math.abs(newScore - node.score);
        maxDiff = Math.max(maxDiff, diff);
        node.score = newScore;
      });

      // 检查收敛
      if (maxDiff < this.config.convergenceThreshold) {
        console.info(`TextRank converged at iteration ${iter + 1}`);
        break;
      }
    }
  }

  // 提取关键词
  extractKeywords(text: string, topN: number = 10): KeywordResult[] {
    const tokens = this.tokenize(text);
    if (tokens.length === 0) return [];

    // 构建词图
    const graph = this.buildGraph(tokens);

    // 运行TextRank算法
    this.runTextRank(graph);

    // 收集结果并排序
    const results: KeywordResult[] = [];
    graph.forEach((node, word) => {
      results.push({
        word,
        score: Math.round(node.score * 10000) / 10000,
        tf: 0,  // TextRank不直接计算TF
        idf: 0, // TextRank不使用IDF
        rank: 0,
      });
    });

    results.sort((a, b) => b.score - a.score);
    return results.slice(0, topN).map((r, idx) => ({ ...r, rank: idx + 1 }));
  }
}

3.3 标签生成与管理系统

/**
 * 标签生成与管理系统
 * 整合TF-IDF和TextRank,支持标签聚类、扩展、推荐
 */

// 标签对象
interface Tag {
  name: string;          // 标签名
  score: number;         // 重要性分数
  source: 'tfidf' | 'textrank' | 'manual' | 'merged';  // 来源
  category: string;      // 标签类别
  synonyms: string[];    // 同义词
  count: number;         // 使用次数
}

// 标签生成配置
interface TagGeneratorConfig {
  tfidfWeight: number;       // TF-IDF权重
  textrankWeight: number;    // TextRank权重
  topN: number;              // 每种算法取TopN
  enableClustering: boolean; // 启用聚类
  enableExpansion: boolean;  // 启用扩展
  minScore: number;          // 最低分数阈值
}

export class TagGenerator {
  private tfidfExtractor: TFIDFExtractor;
  private textrankExtractor: TextRankExtractor;
  private config: TagGeneratorConfig;
  // 同义词词典
  private synonymDict: Map<string, string[]> = new Map();
  // 标签类别映射
  private categoryDict: Map<string, string> = new Map();
  // 全局标签库
  private tagLibrary: Map<string, Tag> = new Map();

  constructor(config?: Partial<TagGeneratorConfig>) {
    this.tfidfExtractor = new TFIDFExtractor();
    this.textrankExtractor = new TextRankExtractor();
    this.config = {
      tfidfWeight: 0.4,
      textrankWeight: 0.6,
      topN: 15,
      enableClustering: true,
      enableExpansion: true,
      minScore: 0.01,
      ...config,
    };
    this.initDictionaries();
  }

  // 初始化同义词和类别词典
  private initDictionaries(): void {
    // 同义词词典
    const synonyms: [string, string[]][] = [
      ['鸿蒙', ['HarmonyOS', 'HMOS']],
      ['开发', ['编程', '程序设计', '写代码']],
      ['框架', ['Framework', '架构']],
      ['组件', ['Component', '控件', '部件']],
      ['性能', ['效率', '速度', '流畅度']],
      ['界面', ['UI', '用户界面', '交互界面']],
      ['数据', ['信息', '资料']],
      ['安全', ['防护', '保护', '加密']],
      ['网络', ['联网', '通信', '连接']],
      ['存储', ['保存', '持久化', '数据库']],
    ];
    synonyms.forEach(([word, syns]) => {
      this.synonymDict.set(word, syns);
    });

    // 类别映射
    const categories: [string, string][] = [
      ['鸿蒙', '平台'], ['HarmonyOS', '平台'],
      ['开发', '活动'], ['编程', '活动'], ['框架', '技术'],
      ['组件', '技术'], ['性能', '质量'], ['界面', '设计'],
      ['数据', '技术'], ['安全', '质量'], ['网络', '技术'],
      ['存储', '技术'], ['动画', '设计'], ['状态', '技术'],
    ];
    categories.forEach(([word, cat]) => {
      this.categoryDict.set(word, cat);
    });
  }

  // 为文本生成标签
  generateTags(text: string, docId?: string): Tag[] {
    // 如果提供了docId,添加到TF-IDF文档集合
    if (docId) {
      this.tfidfExtractor.addDocument(docId, text);
    }

    // TF-IDF提取
    const tfidfKeywords = docId
      ? this.tfidfExtractor.extractKeywords(docId, this.config.topN)
      : this.tfidfExtractor.extractFromText(text, this.config.topN);

    // TextRank提取
    const textrankKeywords = this.textrankExtractor.extractKeywords(text, this.config.topN);

    // 合并两种算法的结果
    const mergedScores = new Map<string, { score: number; source: string }>();

    tfidfKeywords.forEach(kw => {
      const weightedScore = kw.score * this.config.tfidfWeight;
      mergedScores.set(kw.word, { score: weightedScore, source: 'tfidf' });
    });

    textrankKeywords.forEach(kw => {
      const weightedScore = kw.score * this.config.textrankWeight;
      const existing = mergedScores.get(kw.word);
      if (existing) {
        // 两种算法都命中的词,分数叠加
        mergedScores.set(kw.word, {
          score: existing.score + weightedScore,
          source: 'merged',
        });
      } else {
        mergedScores.set(kw.word, { score: weightedScore, source: 'textrank' });
      }
    });

    // 生成标签
    let tags: Tag[] = [];
    mergedScores.forEach((data, word) => {
      if (data.score >= this.config.minScore) {
        tags.push({
          name: word,
          score: Math.round(data.score * 10000) / 10000,
          source: data.source as Tag['source'],
          category: this.categoryDict.get(word) || '其他',
          synonyms: this.synonymDict.get(word) || [],
          count: 1,
        });
      }
    });

    // 标签聚类(合并同义词)
    if (this.config.enableClustering) {
      tags = this.clusterTags(tags);
    }

    // 标签扩展(添加同义词)
    if (this.config.enableExpansion) {
      tags = this.expandTags(tags);
    }

    // 按分数排序
    tags.sort((a, b) => b.score - a.score);

    // 更新全局标签库
    tags.forEach(tag => {
      const existing = this.tagLibrary.get(tag.name);
      if (existing) {
        existing.count++;
        existing.score = Math.max(existing.score, tag.score);
      } else {
        this.tagLibrary.set(tag.name, { ...tag });
      }
    });

    return tags;
  }

  // 标签聚类:合并同义词标签
  private clusterTags(tags: Tag[]): Tag[] {
    const clustered = new Map<string, Tag>();
    const processed = new Set<string>();

    tags.forEach(tag => {
      if (processed.has(tag.name)) return;

      // 查找同义词
      const synonyms = this.synonymDict.get(tag.name) || [];
      const allNames = [tag.name, ...synonyms];

      // 合并同义词标签的分数
      let totalScore = tag.score;
      let bestName = tag.name;

      tags.forEach(otherTag => {
        if (otherTag.name !== tag.name && allNames.includes(otherTag.name)) {
          totalScore += otherTag.score * 0.5; // 同义词贡献一半分数
          processed.add(otherTag.name);
          // 保留分数更高的标签名
          if (otherTag.score > tag.score) {
            bestName = otherTag.name;
          }
        }
      });

      processed.add(tag.name);
      clustered.set(bestName, {
        ...tag,
        name: bestName,
        score: totalScore,
        synonyms: allNames.filter(n => n !== bestName),
        source: 'merged',
      });
    });

    return Array.from(clustered.values());
  }

  // 标签扩展:为标签添加同义词信息
  private expandTags(tags: Tag[]): Tag[] {
    return tags.map(tag => {
      const synonyms = this.synonymDict.get(tag.name) || [];
      return {
        ...tag,
        synonyms: [...new Set([...tag.synonyms, ...synonyms])],
      };
    });
  }

  // 获取全局标签库
  getTagLibrary(): Tag[] {
    return Array.from(this.tagLibrary.values())
      .sort((a, b) => b.count - a.count);
  }

  // 获取热门标签
  getPopularTags(topN: number = 20): Tag[] {
    return this.getTagLibrary().slice(0, topN);
  }

  // 获取标签类别统计
  getCategoryStats(): Map<string, number> {
    const stats = new Map<string, number>();
    this.tagLibrary.forEach(tag => {
      stats.set(tag.category, (stats.get(tag.category) || 0) + tag.count);
    });
    return stats;
  }

  // 手动添加标签
  addManualTag(name: string, category: string = '自定义'): void {
    this.tagLibrary.set(name, {
      name,
      score: 1.0,
      source: 'manual',
      category,
      synonyms: this.synonymDict.get(name) || [],
      count: 1,
    });
  }
}

3.4 标签生成UI组件

/**
 * 关键词提取与标签生成UI组件
 * 展示提取结果、标签云、标签管理
 */
import { TagGenerator, Tag } from './TagGenerator';
import { TFIDFExtractor, KeywordResult } from './TFIDFExtractor';
import { TextRankExtractor } from './TextRankExtractor';

@ObservedV2
class TagViewModel {
  @Trace inputText: string = '';
  @Trace tags: Tag[] = [];
  @Trace tfidfKeywords: KeywordResult[] = [];
  @Trace textrankKeywords: KeywordResult[] = [];
  @Trace isExtracting: boolean = false;
  @Trace popularTags: Tag[] = [];

  private tagGenerator: TagGenerator = new TagGenerator();
  private tfidfExtractor: TFIDFExtractor = new TFIDFExtractor();
  private textrankExtractor: TextRankExtractor = new TextRankExtractor();

  // 提取关键词和生成标签
  extractAndGenerate(): void {
    if (!this.inputText.trim()) return;

    this.isExtracting = true;

    // TF-IDF提取
    this.tfidfKeywords = this.tfidfExtractor.extractFromText(this.inputText, 10);

    // TextRank提取
    this.textrankKeywords = this.textrankExtractor.extractKeywords(this.inputText, 10);

    // 生成标签
    this.tags = this.tagGenerator.generateTags(this.inputText);

    // 获取热门标签
    this.popularTags = this.tagGenerator.getPopularTags(15);

    this.isExtracting = false;
  }

  // 获取标签颜色(按类别)
  getTagColor(category: string): string {
    const colorMap: Record<string, string> = {
      '平台': '#4FC3F7',
      '技术': '#CE93D8',
      '设计': '#FFB74D',
      '质量': '#66BB6A',
      '活动': '#EF5350',
      '其他': '#9E9E9E',
      '自定义': '#80DEEA',
    };
    return colorMap[category] || '#9E9E9E';
  }
}

@Entry
@Component
struct KeywordExtractionPage {
  private viewModel: TagViewModel = new TagViewModel();

  build() {
    Scroll() {
      Column() {
        // 标题
        this.TitleBar()

        // 输入区
        this.InputArea()

        // 算法对比
        this.AlgorithmComparison()

        // 标签云
        this.TagCloud()

        // 热门标签
        this.PopularTags()
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1A1A2E')
    .scrollBar(BarState.Auto)
  }

  @Builder
  TitleBar() {
    Row() {
      Column() {
        Text('关键词提取与标签生成')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
        Text('TF-IDF + TextRank 双算法融合')
          .fontSize(13)
          .fontColor('#9E9E9E')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16, bottom: 16 })
  }

  @Builder
  InputArea() {
    Column() {
      TextArea({ placeholder: '输入文本进行关键词提取...' })
        .width('100%')
        .height(120)
        .fontSize(16)
        .fontColor('#E0E0E0')
        .backgroundColor('#16213E')
        .borderRadius(12)
        .padding(12)
        .onChange((value: string) => {
          this.viewModel.inputText = value;
        })

      Button('提取关键词 & 生成标签')
        .width('100%')
        .height(48)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#4FC3F7')
        .fontColor('#1A1A2E')
        .borderRadius(12)
        .margin({ top: 12 })
        .enabled(!this.viewModel.isExtracting)
        .onClick(() => {
          this.viewModel.extractAndGenerate();
        })
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 8, bottom: 16 })
  }

  // 算法对比展示
  @Builder
  AlgorithmComparison() {
    if (this.viewModel.tfidfKeywords.length > 0 || this.viewModel.textrankKeywords.length > 0) {
      Column() {
        Text('算法对比')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
          .margin({ bottom: 12 })

        // TF-IDF结果
        Text('TF-IDF 关键词')
          .fontSize(14)
          .fontColor('#4FC3F7')
          .margin({ bottom: 8 })

        Row() {
          ForEach(this.viewModel.tfidfKeywords.slice(0, 8), (kw: KeywordResult) => {
            Column() {
              Text(kw.word)
                .fontSize(13)
                .fontColor('#E0E0E0')
              Text(kw.score.toFixed(3))
                .fontSize(10)
                .fontColor('#9E9E9E')
                .margin({ top: 2 })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor('#16213E')
            .borderRadius(8)
            .margin({ right: 6, bottom: 6 })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)

        // TextRank结果
        Text('TextRank 关键词')
          .fontSize(14)
          .fontColor('#CE93D8')
          .margin({ top: 12, bottom: 8 })

        Row() {
          ForEach(this.viewModel.textrankKeywords.slice(0, 8), (kw: KeywordResult) => {
            Column() {
              Text(kw.word)
                .fontSize(13)
                .fontColor('#E0E0E0')
              Text(kw.score.toFixed(3))
                .fontSize(10)
                .fontColor('#9E9E9E')
                .margin({ top: 2 })
            }
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor('#2D0033')
            .borderRadius(8)
            .margin({ right: 6, bottom: 6 })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#0F3460')
      .borderRadius(16)
      .margin({ left: 20, right: 20, top: 8 })
    }
  }

  // 标签云
  @Builder
  TagCloud() {
    if (this.viewModel.tags.length > 0) {
      Column() {
        Text('生成标签')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
          .margin({ bottom: 12 })

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.viewModel.tags, (tag: Tag) => {
            Row() {
              Text(tag.name)
                .fontSize(this.getTagFontSize(tag.score))
                .fontColor(this.viewModel.getTagColor(tag.category))
              
              if (tag.synonyms.length > 0) {
                Text(` (${tag.synonyms.length}个同义词)`)
                  .fontSize(10)
                  .fontColor('#666')
              }
            }
            .padding({ left: 14, right: 14, top: 8, bottom: 8 })
            .backgroundColor('#16213E')
            .borderRadius(20)
            .border({ width: 1, color: this.viewModel.getTagColor(tag.category) + '40' })
            .margin({ right: 8, bottom: 8 })
          })
        }
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#0F3460')
      .borderRadius(16)
      .margin({ left: 20, right: 20, top: 12 })
    }
  }

  // 根据分数计算标签字号
  private getTagFontSize(score: number): number {
    if (score > 0.5) return 18;
    if (score > 0.3) return 16;
    if (score > 0.1) return 14;
    return 12;
  }

  // 热门标签
  @Builder
  PopularTags() {
    if (this.viewModel.popularTags.length > 0) {
      Column() {
        Text('热门标签')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
          .margin({ bottom: 12 })

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.viewModel.popularTags, (tag: Tag) => {
            Row() {
              Circle().width(6).height(6).fill(this.viewModel.getTagColor(tag.category))
              Text(tag.name)
                .fontSize(13)
                .fontColor('#E0E0E0')
                .margin({ left: 6 })
              Text(`×${tag.count}`)
                .fontSize(11)
                .fontColor('#666')
                .margin({ left: 4 })
            }
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor('#16213E')
            .borderRadius(16)
            .margin({ right: 8, bottom: 8 })
          })
        }
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#0F3460')
      .borderRadius(16)
      .margin({ left: 20, right: 20, top: 12, bottom: 20 })
    }
  }
}

四、踩坑与注意事项

4.1 中文分词的质量决定关键词质量

问题:TF-IDF和TextRank都依赖分词结果。如果分词不准,关键词提取就是"垃圾进垃圾出"。比如"鸿蒙开发"被分成"鸿"+“蒙”+“开”+“发”,那提取出来的关键词就毫无意义。

解决方案

  • 优先使用HarmonyOS NLP API的分词能力
  • 构建领域专属词典,提升专业术语的分词准确率
  • 对分词结果进行后处理,合并被错误拆分的专有名词

4.2 IDF的计算偏差

问题:当文档集合很小时,IDF值不稳定。比如只有2篇文档时,一个词在1篇中出现,IDF = log(3/2) ≈ 0.4,区分度很低。

解决方案

  • 使用平滑IDF公式:IDF = log((N+1)/(df+1)) + 1
  • 文档集合少于10篇时,使用预训练的IDF值
  • 定期用新文档更新IDF,保持统计量的时效性

4.3 TextRank的窗口大小选择

问题:窗口太小,词间关系捕获不足;窗口太大,无关词之间也会建立边,引入噪声。

解决方案

  • 短文本(<100字)用窗口3-5
  • 中等文本(100-500字)用窗口5-8
  • 长文本(>500字)用窗口8-12
  • 可以通过实验调优,观察不同窗口大小下关键词的准确率

4.4 标签爆炸问题

问题:当处理大量文档时,标签数量会急剧膨胀,导致标签库难以管理。

解决方案

  • 设置最低分数阈值,过滤低质量标签
  • 定期合并语义相近的标签
  • 限制标签库总量(建议不超过500个活跃标签)
  • 实现标签的层级结构(父标签-子标签),而非扁平列表

五、HarmonyOS 6适配

5.1 API变更

能力 HarmonyOS 5.0 HarmonyOS 6.0
实体识别 基础NER(人名/地名/组织) 增强NER,支持领域实体
关键词提取 无内置API 新增端侧关键词提取API
语义相似度 不支持 新增句向量与语义相似度计算
词向量 不支持 新增端侧词向量查询

5.2 迁移指南

// HarmonyOS 5.0:需要手动实现TF-IDF/TextRank
// (如本文代码所示)

// HarmonyOS 6.0:可使用内置API
import { nlp } from '@kit.AiKit';

// 关键词提取
const extractor = nlp.createKeywordExtractor({
  algorithm: 'hybrid',  // 'tfidf' | 'textrank' | 'hybrid'
  topN: 10,
});
const keywords = await extractor.extract(text);
extractor.destroy();

// 语义相似度(用于标签聚类)
const similarity = nlp.computeSimilarity('鸿蒙开发', 'HarmonyOS编程');
// similarity: 0.87

5.3 新特性适配建议

  • 端侧词向量:利用词向量进行更精准的标签聚类,替代基于同义词词典的简单聚类
  • 语义相似度:用于标签去重和合并,解决"鸿蒙"和"HarmonyOS"是同一标签的问题
  • 领域实体识别:自动识别文本中的专业术语,作为标签候选

六、总结

知识点 核心内容
TF-IDF算法 词频×逆文档频率,简单高效,适合基线方案
TextRank算法 基于图排序,考虑词间共现关系,效果优于TF-IDF
双算法融合 TF-IDF权重0.4 + TextRank权重0.6,取长补短
标签聚类 基于同义词词典合并语义相近的标签
标签扩展 为标签添加同义词信息,提升检索召回率
分词质量 中文分词是关键词提取的基础,决定最终效果
IDF平滑 使用log((N+1)/(df+1))+1避免小数据集偏差
标签管理 最低分数阈值+定期合并+层级结构,防止标签爆炸

核心思想:关键词提取不是"选词",而是"排序"——从所有可能的词中,按照重要性排序选出最有代表性的。TF-IDF看"独特性",TextRank看"连接性",两者融合才能既独特又有连接。标签生成则是在关键词基础上的"升华"——聚类让标签更精炼,扩展让标签更丰富。

实践建议

  1. 先用TF-IDF快速验证,确认分词质量没问题后再引入TextRank
  2. 同义词词典是标签聚类的关键,投入时间维护它比调算法参数更有效
  3. 标签数量控制在合理范围,"少而精"比"多而杂"更有价值
  4. 定期人工审核标签质量,算法不能完全替代人的判断
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。