【每日一题】Python 编程题:实现模型输出一致性打分(归一化编辑距离相似度,附题解)

题目:AI 生成用例/模型回答的自动化回归里,需要用「与人工标准答案的相似度」做卡口指标。请实现 text_similarity:

  1. 归一化:转小写,并删除所有空白字符(半角空格、制表符、换行、全角空格);
  2. 计算归一化后两个字符串的编辑距离(插入、删除、替换代价均为 1);
  3. 相似度 = 1 - 编辑距离 / max(len(a), len(b));
  4. 两者归一化后均为空串时返回 1.0。
def text_similarity(reference: str, prediction: str) -> float:
    """计算模型输出与参考答案的归一化编辑距离相似度。

    Args:
        reference: 人工标准答案
        prediction: 模型实际输出
    Returns:
        0.0 ~ 1.0 之间的相似度;两串归一化后均为空时返回 1.0
    """
    pass


assert text_similarity("Hello World", "helloworld") == 1.0
assert text_similarity("", "") == 1.0
assert abs(text_similarity("kitten", "sitting") - (1 - 3 / 7)) < 1e-9
assert text_similarity("abc", "") == 0.0
assert text_similarity("测试 用例", "测试用例") == 1.0
assert abs(text_similarity("登录成功", "登陆成功") - 0.75) < 1e-9

难度:高级
题型:笔试(编程题)

:point_right:参考答案/题解:

思路一:自顶向下递归 + 记忆化(先把「最后字符相同就跳过、不同就三种操作取最小」写成递归,再补 lru_cache,直观好讲)

import re
from functools import lru_cache


def normalize(text: str) -> str:
    return re.sub(r"\s+", "", text).lower()


def text_similarity_v1(reference: str, prediction: str) -> float:
    a, b = normalize(reference), normalize(prediction)

    @lru_cache(maxsize=None)
    def dfs(i: int, j: int) -> int:
        if i == 0:
            return j
        if j == 0:
            return i
        if a[i - 1] == b[j - 1]:
            return dfs(i - 1, j - 1)
        return 1 + min(dfs(i - 1, j), dfs(i, j - 1), dfs(i - 1, j - 1))

    longest = max(len(a), len(b))
    if longest == 0:
        return 1.0
    return 1 - dfs(len(a), len(b)) / longest

思路二:滚动数组 DP(推荐,空间 O(min(m, n)))

import re


def text_similarity(reference: str, prediction: str) -> float:
    a, b = re.sub(r"\s+", "", reference).lower(), re.sub(r"\s+", "", prediction).lower()
    if len(a) < len(b):
        a, b = b, a                      # 短串当列,省内存
    m, n = len(a), len(b)
    if m == 0:
        return 1.0                       # 两个都是空串
    prev = list(range(n + 1))
    for i in range(1, m + 1):
        cur = [i] + [0] * n
        for j in range(1, n + 1):
            if a[i - 1] == b[j - 1]:
                cur[j] = prev[j - 1]     # 代价 0,直接继承左上角
            else:
                cur[j] = 1 + min(prev[j], cur[j - 1], prev[j - 1])
        prev = cur
    return 1 - prev[n] / m

面试现场验证过,assert 全部通过:kitten/sitting = 0.5714285714285714,登录成功/登陆成功 = 0.75,"abc"/"" = 0.0,""/"" = 1.0。

采分点:

  • 归一化写法:re.sub(r"\s+", "", s).lower(),\s 在 Python3 下能匹配全角空格 \u3000,别用 s.strip()(只去首尾,中间空格留着就错)。
  • 空串边界:两串都空要返回 1.0,必须在除法前判断,否则 ZeroDivisionError。
  • 分母取 max(len(a), len(b)),取 sum 或 min 都会让相似度失真。
  • DP 递推式讲清三种操作的含义:prev[j] 删除、cur[j-1] 插入、prev[j-1] 替换;字符相等时左上角代价为 0,不是 1。
  • 复杂度:时间 O(mn);朴素二维表空间 O(mn),滚动数组优化到 O(min(m,n))。纯递归不记忆化是 O(3^(m+n)),现场会被判不通过。
  • 工程延伸(加分):中文按字符切分粒度偏细,实际项目可用分词后词级编辑距离或 ROUGE / BERTScore;相似度卡口要按业务分档(如 ≥0.9 视为通过),而不是只看单一阈值。