前言
我是字笺-SinoName的算法开发者。今天想和大家分享一下我们古诗文起名算法的设计思路和演进过程。这是一个将传统文化与现代技术结合的项目,涉及NLP、规则引擎、多维评分等多个技术领域。
字笺-SinoName 是一个基于古诗文的起名小程序,核心功能是从《诗经》《楚辞》等典籍中提取适合作为名字的词组,并进行多维评分。
问题定义
输入输出
输入:
- 姓氏(如"张")
- 典籍来源(如"诗经")
- 性别偏好(男/女/不限)
输出:
- 6个名字候选(每个包含:名字、出处、寓意解释)
约束条件
- 语义通顺:名字要有意义,不能生硬拼凑
- 音律优美:平仄协调,避免拗口
- 字形美观:笔画适中,避免生僻字
- 文化契合:符合典籍意境
第一版:纯规则引擎
设计思路
最初的版本,我们采用了纯规则引擎:
// 规则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. 寓意评分(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(当前版本)
动机
纯规则引擎 + 评分模型虽然效果不错,但仍有局限:
- 语义理解不足:无法理解深层文化内涵
- 评分权重固定:无法根据用户偏好调整
- 缺乏个性化:所有用户看到的评分标准相同
引入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 | 中 |
| 引入NLP | 85% | 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. 音律评分误差
问题:普通话声调和古音声调不一致
解决:使用《平水韵》声调表
后续规划
- 引入大语言模型:用GPT-4增强语义理解
- 实时学习:根据用户点击反馈优化模型
- 多语言支持:英文名、双语名
- 个性化推荐:根据用户偏好推荐
总结
从纯规则引擎到引入NLP,算法演进的过程也是我们团队技术成长的过程。几点经验:
- 规则是基础:先建立规则,再引入模型
- 可解释性重要:用户需要知道为什么这个名字好
- 数据驱动优化:根据用户反馈持续迭代
- 性能与效果平衡:不要为了效果牺牲太多性能
如果你对算法细节感兴趣,或者对起名有需求,欢迎搜索体验 字笺-SinoName,也欢迎在评论区交流技术问题。