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%。
| 故障簇 | 原始唯一写法数 | 归一后唯一写法数 | 一致性提升 |
|---|---|---|---|
| 推进器过流 | 4 | 1 | 4 → 1 |
| 深度超限 | 2 | 1 | 2 → 1 |
| DVL 数据丢失 | 2 | 1 | 2 → 1 |
| 水声通信中断 | 2 | 1 | 2 → 1 |
SYNONYM_MAP 与 UNIT_RULES 纳入版本管理。词典一旦变更,需重跑一致性指标,避免“归一化规则漂移”导致训练集与推理集特征分布不一致。规则执行顺序与风险点
顺序错误会引入不可逆损失:若先做同义词合并再单位归一,12.5安 中的“安”可能被误当作独立词处理;若先小写再全角转半角,全角数字 12.5 无法被数值正则捕获。因此固定为:NFKC 归一 → 小写 → 空白压缩 → 单位归一 → 同义词合并 → 二次空白压缩。
(?<![a-z0-9_]),可避免 oc 误匹配 dock 内部;但中文无空格分词,需保证中文同义词条目本身是完整词,否则会出现子串误替换。上线前应对词典做一次“误替换回归测试”。验证与通过条件
标准化是否达标,用三条可执行判据检查:
- 簇内唯一性:同一故障簇(如推进器过流)归一后唯一写法数应为 1;若大于 1,说明词典缺条目。
- 数值保真:归一后数值与单位必须可被正则
(\d+(?:\.\d+)?)(m|A|V|s|C)完整捕获,且数值不变。 - 误替换回归:对词典中每个短缩写(如
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 才能作为后续分类的稳定输入,避免同一故障因写法差异被拆成多个特征。