4. 故障日志文本字段的标准化与归一化

为什么自由文本字段必须先标准化

水下机器人航次日志中的“故障现象”和“操作员备注”通常由不同班次、不同岗位的人员手写或口述录入,同一物理事件会出现多种写法。例如推进器过流可能被写成 Thruster OverCurrent、推进器过流、推进器电流过大、THR over current;深度异常可能写成 depth 12.5m、深度12.5米、Depth=12.5 M。如果直接把这些原始字符串送入特征提取或分类器,同一故障会被拆成多个稀疏特征,导致类别特征分布被稀释、词表膨胀、模型难以收敛。

本节的处理对象是日志中的自由文本列,处理目标是产出一个可复现、可审计的标准化文本字段 desc_norm,并量化归一化前后特征一致性的提升。核心动作有三类:大小写与空白统一、单位与数值归一、同义词与缩写合并。

标准化规则设计

规则按“先无损、后有损”的顺序执行,避免单位归一破坏同义词匹配所需的原始形态。下面给出一个可直接运行的 Python 实现,输入为示例日志记录,输出标准化字段与一致性指标。

import re
import unicodedata
from collections import Counter

# ---------- 1. 同义词/缩写映射表(领域词典,可扩展) ----------
SYNONYM_MAP = {
    # 推进系统
    "thr": "thruster", "推进器": "thruster", "推力器": "thruster",
    "overcurrent": "over_current", "过流": "over_current", "电流过大": "over_current",
    "over current": "over_current", "oc": "over_current",
    # 导航/深度
    "dvl": "dvl", "多普勒": "dvl",
    "depth": "depth", "深度": "depth",
    "ins": "ins", "惯导": "ins", "惯性导航": "ins",
    # 通信
    "acoustic modem": "acoustic_modem", "水声modem": "acoustic_modem",
    "水声通信": "acoustic_modem", "声通": "acoustic_modem",
    "comm": "communication", "通信": "communication", "通讯": "communication",
    # 电源
    "batt": "battery", "电池": "battery", "蓄电池": "battery",
    "low voltage": "low_voltage", "低压": "low_voltage", "电压低": "low_voltage",
    # 传感器
    "leak": "leak", "漏水": "leak", "渗水": "leak",
    "imu": "imu", "姿态传感器": "imu",
}

# ---------- 2. 单位归一:统一到 SI 基准 ----------
UNIT_RULES = [
    # 深度/距离 -> m
    (r"(\d+(?:\.\d+)?)\s*(?:米|m|M)(?![a-zA-Z])", r"\1m"),
    # 电流 -> A
    (r"(\d+(?:\.\d+)?)\s*(?:安|A|a)(?![a-zA-Z])", r"\1A"),
    # 电压 -> V
    (r"(\d+(?:\.\d+)?)\s*(?:伏|V|v)(?![a-zA-Z])", r"\1V"),
    # 时间 -> s
    (r"(\d+(?:\.\d+)?)\s*(?:秒|s|S)(?![a-zA-Z])", r"\1s"),
    # 温度 -> C
    (r"(\d+(?:\.\d+)?)\s*(?:摄氏度|℃|°C|C)(?![a-zA-Z])", r"\1C"),
]

def normalize_text(raw: str) -> str:
    if raw is None:
        return ""
    s = unicodedata.normalize("NFKC", str(raw))   # 全角转半角
    s = s.strip().lower()                          # 大小写统一
    s = re.sub(r"\s+", " ", s)                     # 空白压缩
    # 单位归一(在分词前做,保留数值)
    for pat, rep in UNIT_RULES:
        s = re.sub(pat, rep, s)
    # 同义词合并:按词边界替换,长词优先
    for k in sorted(SYNONYM_MAP, key=len, reverse=True):
        s = re.sub(rf"(?<![a-z0-9_]){re.escape(k)}(?![a-z0-9_])",
                   SYNONYM_MAP[k], s)
    s = re.sub(r"\s+", " ", s).strip()
    return s

# ---------- 3. 示例日志 ----------
logs = [
    {"id": "L01", "desc": "Thruster OverCurrent 12.5A"},
    {"id": "L02", "desc": "推进器过流 12.5安"},
    {"id": "L03", "desc": "THR over current 12.5 A"},
    {"id": "L04", "desc": "推进器电流过大 12.5A"},
    {"id": "L05", "desc": "Depth 12.5米 超限"},
    {"id": "L06", "desc": "深度12.5m 超限"},
    {"id": "L07", "desc": "DVL 数据丢失"},
    {"id": "L08", "desc": "多普勒 数据丢失"},
    {"id": "L09", "desc": "水声Modem 通信中断"},
    {"id": "L10", "desc": "声通 comm 中断"},
]

for r in logs:
    r["desc_norm"] = normalize_text(r["desc"])

for r in logs:
    print(f'{r["id"]} | {r["desc"]:<28} -> {r["desc_norm"]}')

运行后 L01–L04 会归并为同一标准化描述 thruster over_current 12.5a,L05–L06 归并为 depth 12.5m 超限,L07–L08 归并为 dvl 数据丢失,L09–L10 归并为 acoustic_modem communication 中断。这正是后续分类特征一致性的来源。

归一化前后特征一致性评估

用“同一故障簇内标准化文本的唯一值数量”和“词表规模”两个指标衡量。归一化前,4 条推进器过流记录产生 4 个不同字符串;归一化后只剩 1 个。词表规模从原始 10 条记录的 10 个唯一串,压缩到 4 个唯一串,压缩率 60%。

故障簇原始唯一写法数归一后唯一写法数一致性提升
推进器过流414 → 1
深度超限212 → 1
DVL 数据丢失212 → 1
水声通信中断212 → 1
评估时建议固定随机种子与词典版本,把 SYNONYM_MAP 与 UNIT_RULES 纳入版本管理。词典一旦变更,需重跑一致性指标,避免“归一化规则漂移”导致训练集与推理集特征分布不一致。

规则执行顺序与风险点

顺序错误会引入不可逆损失:若先做同义词合并再单位归一,12.5安 中的“安”可能被误当作独立词处理;若先小写再全角转半角,全角数字 12.5 无法被数值正则捕获。因此固定为:NFKC 归一 → 小写 → 空白压缩 → 单位归一 → 同义词合并 → 二次空白压缩。

同义词合并使用词边界断言 (?<![a-z0-9_]),可避免 oc 误匹配 dock 内部;但中文无空格分词,需保证中文同义词条目本身是完整词,否则会出现子串误替换。上线前应对词典做一次“误替换回归测试”。
自由文本标准化处理链与特征一致性对比 原始自由文本 (大小写/单位/同义词混杂) Thruster OverCurrent 12.5A 推进器过流 12.5安 THR over current 12.5 A 推进器电流过大 12.5A Depth 12.5米 超限 深度12.5m 超限 DVL 数据丢失 多普勒 数据丢失 水声Modem 通信中断 声通 comm 中断 标准化处理链(固定顺序) ① NFKC 归一 全角数字/符号 → 半角 ② 小写 + 空白压缩 大小写统一,多空格合并 ③ 单位归一 米/m→m,安/A→A,伏/V→V ④ 同义词/缩写合并 thr/推进器→thruster,过流→over_current ⑤ 二次空白压缩 输出 desc_norm 标准化字段 desc_norm (唯一写法数大幅压缩) thruster over_current 12.5a depth 12.5m 超限 (2 条 → 1 条) dvl 数据丢失 (2 条 → 1 条) acoustic_modem communication 中断 一致性指标 原始唯一串:10 归一后唯一串:4 词表压缩率:60% 簇内写法:4→1 顺序不可颠倒:单位归一必须在同义词合并之前,否则数值单位会被词边界规则破坏

验证与通过条件

标准化是否达标,用三条可执行判据检查:

  1. 簇内唯一性:同一故障簇(如推进器过流)归一后唯一写法数应为 1;若大于 1,说明词典缺条目。
  2. 数值保真:归一后数值与单位必须可被正则 (\d+(?:\.\d+)?)(m|A|V|s|C) 完整捕获,且数值不变。
  3. 误替换回归:对词典中每个短缩写(如 oc、ins)构造包含该子串的正常词(dock、inside),确认不被替换。

把上述检查写成断言,即可在每次词典更新后自动回归:

def check_consistency(records, cluster_key):
    groups = {}
    for r in records:
        groups.setdefault(cluster_key(r), set()).add(r["desc_norm"])
    return {k: len(v) for k, v in groups.items()}

# 示例:按故障簇标签检查
cluster = {"L01":"thr_oc","L02":"thr_oc","L03":"thr_oc","L04":"thr_oc",
           "L05":"depth","L06":"depth","L07":"dvl","L08":"dvl",
           "L09":"comm","L10":"comm"}
result = check_consistency(logs, lambda r: cluster[r["id"]])
assert all(v == 1 for v in result.values()), f"存在未归并簇: {result}"
print("一致性检查通过:", result)

该断言在示例数据上应输出每个簇的唯一写法数均为 1。若某簇大于 1,定位到该簇的原始记录,补充同义词条目后重跑,直到全部通过。这样标准化字段 desc_norm 才能作为后续分类的稳定输入,避免同一故障因写法差异被拆成多个特征。