古诗文起名算法设计:从规则引擎到评分模型的演进

3 阅读5分钟

前言

我是字笺-SinoName的算法开发者。今天想和大家分享一下我们古诗文起名算法的设计思路和演进过程。这是一个将传统文化与现代技术结合的项目,涉及NLP、规则引擎、多维评分等多个技术领域。

字笺-SinoName 是一个基于古诗文的起名小程序,核心功能是从《诗经》《楚辞》等典籍中提取适合作为名字的词组,并进行多维评分。

问题定义

输入输出

输入

  • 姓氏(如"张")
  • 典籍来源(如"诗经")
  • 性别偏好(男/女/不限)

输出

  • 6个名字候选(每个包含:名字、出处、寓意解释)

约束条件

  1. 语义通顺:名字要有意义,不能生硬拼凑
  2. 音律优美:平仄协调,避免拗口
  3. 字形美观:笔画适中,避免生僻字
  4. 文化契合:符合典籍意境

第一版:纯规则引擎

设计思路

最初的版本,我们采用了纯规则引擎:

// 规则1:相邻词组合
function extractAdjacent(sentence) {
  const words = tokenize(sentence);
  const candidates = [];
  
  for (let i = 0; i < words.length - 1; i++) {
    if (isMeaningful(words[i]) && isMeaningful(words[i+1])) {
      candidates.push(words[i].word + words[i+1].word);
    }
  }
  
  return candidates;
}

// 规则2:过滤生僻字
function filterRareWords(name) {
  const RARE_WORDS = ['龘', '靐', '齉', '爩'];
  return !name.split('').some(char => RARE_WORDS.has(char));
}

// 规则3:过滤贬义词
function filterNegativeWords(name) {
  const NEGATIVE = ['死', '亡', '病', '灾', '祸'];
  return !name.split('').some(char => NEGATIVE.has(char));
}

优点

  1. 可解释性强:每条规则清晰可见
  2. 易于调整:修改规则立即生效
  3. 性能高:纯计算,无模型推理

缺点

  1. 规则难以穷尽:总有遗漏的情况
  2. 评分主观:无法量化"好名字"
  3. 维护成本高:规则越来越多,难以管理

第二版:规则引擎 + 评分模型

架构升级

输入 → 规则引擎(提取候选) → 评分模型(多维评估) → 排序输出

评分模型设计

我们设计了三个维度的评分:

1. 寓意评分(40%)

const MEANING_DICT = {
  '清': ['清澈', '高洁', '清廉'],
  '明': ['明亮', '智慧', '光明'],
  '华': ['华丽', '才华', '繁盛'],
  // ...
};

function scoreMeaning(name) {
  let score = 0;
  const explanations = [];
  
  for (const char of name) {
    if (MEANING_DICT[char]) {
      score += 10;
      explanations.push(MEANING_DICT[char][0]);
    }
  }
  
  return { score: Math.min(score, 100), explanations };
}

2. 音律评分(35%)

const TONES = { '1': '平', '2': '平', '3': '仄', '4': '仄' };

function scorePhonetics(name, surname) {
  const full = surname + name;
  const tones = getTones(full);
  const pingze = tones.map(t => TONES[t]);
  
  let score = 50;
  
  // 平仄协调加分
  if (isGoodPingze(pingze)) score += 20;
  
  // 避免拗口加分
  if (!isAukou(full)) score += 15;
  
  // 双声叠韵加分
  if (isShuangSheng(full) || isDieYun(full)) score += 15;
  
  return { score: Math.min(score, 100), tones: pingze.join('') };
}

3. 字形评分(25%)

function scoreStructure(name) {
  const strokes = name.split('').map(char => getStrokes(char));
  let score = 50;
  
  // 笔画均衡
  const diff = Math.max(...strokes) - Math.min(...strokes);
  if (diff <= 5) score += 20;
  
  // 总笔画适中
  const total = strokes.reduce((a, b) => a + b, 0);
  if (total >= 12 && total <= 20) score += 20;
  
  return { score: Math.min(score, 100), strokes };
}

综合评分

function scoreCandidate(name, surname) {
  const meaning = scoreMeaning(name);
  const phonetics = scorePhonetics(name, surname);
  const structure = scoreStructure(name);
  
  const totalScore = 
    meaning.score * 0.4 +
    phonetics.score * 0.35 +
    structure.score * 0.25;
  
  return {
    name: surname + name,
    score: Math.round(totalScore),
    breakdown: {
      meaning: meaning.score,
      phonetics: phonetics.score,
      structure: structure.score
    },
    explanations: meaning.explanations
  };
}

第三版:引入NLP(当前版本)

动机

纯规则引擎 + 评分模型虽然效果不错,但仍有局限:

  1. 语义理解不足:无法理解深层文化内涵
  2. 评分权重固定:无法根据用户偏好调整
  3. 缺乏个性化:所有用户看到的评分标准相同

引入BERT

我们引入了预训练模型来增强语义理解:

// 使用预训练的BERT模型进行语义评估
async function evaluateWithBERT(name, context) {
  const embedding = await getEmbedding(name);
  const contextEmbedding = await getEmbedding(context);
  
  // 计算语义相似度
  const similarity = cosineSimilarity(embedding, contextEmbedding);
  
  return similarity * 100;
}

个性化评分

async function scoreWithPersonalization(name, userPreferences) {
  const baseScore = scoreCandidate(name);
  
  // 根据用户偏好调整权重
  const weights = {
    meaning: userPreferences.meaningWeight || 0.4,
    phonetics: userPreferences.phoneticsWeight || 0.35,
    structure: userPreferences.structureWeight || 0.25
  };
  
  const personalizedScore = 
    baseScore.breakdown.meaning * weights.meaning +
    baseScore.breakdown.phonetics * weights.phonetics +
    baseScore.breakdown.structure * weights.structure;
  
  return { ...baseScore, personalizedScore };
}

实际效果

版本对比

版本准确率(人工评估)响应时间维护成本
纯规则引擎62%50ms
规则+评分模型78%120ms
引入NLP85%300ms

用户反馈

上线后,我们收集了用户反馈:

  • 正面

    • "名字很有诗意,符合我的期待"
    • "评分很合理,帮我做了选择"
    • "出处解释很详细,学到了很多"
  • 负面

    • "有时候生成的字太生僻"
    • "希望能有更多名字可选"

算法优化

1. 缓存策略

// 语料缓存24小时
const CACHE_TTL = 24 * 60 * 60 * 1000;

function getWithCache(key, fetchFn) {
  const cached = cache.get(key);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }
  
  const data = fetchFn();
  cache.set(key, { data, timestamp: Date.now() });
  return data;
}

2. 批量处理

// 批量提取,减少重复分词
function batchExtract(poems) {
  const candidates = [];
  
  for (const poem of poems) {
    const sentences = poem.content.split(/[。!?]/);
    for (const sentence of sentences) {
      candidates.push(...extractCandidates(sentence));
    }
  }
  
  return candidates;
}

3. 并发处理

// 并发获取多个典籍的语料
async function getMultipleSources(sources) {
  const promises = sources.map(source => getPoems(source));
  const results = await Promise.all(promises);
  return results.flat();
}

踩坑记录

1. 变量命名冲突

问题:变量名用debug,和debug.js模块名冲突,编译后变成undefined.log()

解决:统一用dbg代替debug

2. 生僻字问题

问题:用户反馈名字显示为"□"

解决:添加生僻字过滤

const RARE_WORDS = new Set(['龘', '靐', '齉', '爩']);

function filterRareWords(name) {
  return !name.split('').some(char => RARE_WORDS.has(char));
}

3. 音律评分误差

问题:普通话声调和古音声调不一致

解决:使用《平水韵》声调表

后续规划

  1. 引入大语言模型:用GPT-4增强语义理解
  2. 实时学习:根据用户点击反馈优化模型
  3. 多语言支持:英文名、双语名
  4. 个性化推荐:根据用户偏好推荐

总结

从纯规则引擎到引入NLP,算法演进的过程也是我们团队技术成长的过程。几点经验:

  1. 规则是基础:先建立规则,再引入模型
  2. 可解释性重要:用户需要知道为什么这个名字好
  3. 数据驱动优化:根据用户反馈持续迭代
  4. 性能与效果平衡:不要为了效果牺牲太多性能

如果你对算法细节感兴趣,或者对起名有需求,欢迎搜索体验 字笺-SinoName,也欢迎在评论区交流技术问题。