HarmonyOS 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——如果很多重要的词都和某个词相邻,那这个词也很重要。

TextRank的核心公式:
WS(Vi) = (1-d) + d × Σ(Vj∈In(Vi)) [ wji / Σ(Vk∈Out(Vj)) wjk ] × WS(Vj)
其中 d 是阻尼系数(通常取0.85),wji 是词Vj到Vi的边权重。
2.4 标签生成的完整流程
关键词提取只是第一步,标签生成还需要:
- 关键词清洗:去除停用词、低频词、无意义词
- 语义聚类:将语义相近的关键词合并为一个标签
- 标签扩展:基于同义词词典和上下位关系扩展标签
- 标签排序:根据重要性和多样性排序
- 标签去重:去除语义重复的标签
三、代码实战
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看"连接性",两者融合才能既独特又有连接。标签生成则是在关键词基础上的"升华"——聚类让标签更精炼,扩展让标签更丰富。
实践建议:
- 先用TF-IDF快速验证,确认分词质量没问题后再引入TextRank
- 同义词词典是标签聚类的关键,投入时间维护它比调算法参数更有效
- 标签数量控制在合理范围,"少而精"比"多而杂"更有价值
- 定期人工审核标签质量,算法不能完全替代人的判断
- 点赞
- 收藏
- 关注作者
评论(0)