C---深度学习-八-
C++ 深度学习(八)
译者:飞龙
// trace.hpp
pragma once
include "metrics.hpp"
include "log.hpp"
include
include
struct Span {
const char* name;
std::string req_id, trace_id, model, version, device;
Registry* reg;
std::chrono::high_resolution_clock::time_point t0;
Span(const char* n,
std::string req,
std::string trace,
std::string m,
std::string v,
std::string dev,
- Registry* r)
-
name(n),
req_id(std::move(req)),
trace_id(std::move(trace)),
model(std::move(m)),
version(std::move(v)),
device(std::move(dev)),
reg(r),
t0(std::chrono::high_resolution_clock::now()) {}
~Span() {
using namespace std::chrono;
auto us = duration_cast
high_resolution_clock::now() - t0).count();
log_json("TRACE", "span", req_id, trace_id, model, version, device, 1,
std::string("{"span":"") + name + "","dur_us":" +
std::to_string(us) + "}");
}
};
## 第 12 章:监控已部署模型
```cpp
if (reg) {
Labels L;
L.kv = {{"model", model},
{"version", version},
{"span", name},
{"device", device}};
reg->histo("span_latency_us", L).observe((uint64_t)us);
}
}
};
在请求处理器内部使用:
void handle_request(const Request& r, Registry& R) {
Span s_all("request_total", r.id, r.trace, "m1", "1.3", r.device, &R);
{ Span s("parse", r.id, r.trace, "m1", "1.3", r.device, &R); }
{ Span s("preprocess", r.id, r.trace, "m1", "1.3", r.device, &R); }
{ Span s("queue", r.id, r.trace, "m1", "1.3", r.device, &R); }
{ Span s("infer", r.id, r.trace, "m1", "1.3", r.device, &R); }
{ Span s("postprocess", r.id, r.trace, "m1", "1.3", r.device, &R); }
{ Span s("serialize", r.id, r.trace, "m1", "1.3", r.device, &R); }
}
这些 span 同时对指标和日志形成补充。如果 queue 增长而 infer 保持平稳,可能的修复措施是批处理或并发。如果 preprocess 出现尖峰,那么分词或转换更有可能是罪魁祸首。
将各个部分整合在一起
图 12.8 展示了完整的下游路径:服务发出指标,指标服务器抓取并存储指标,仪表盘查询指标,告警被向外路由。日志和 span 通过在服务内部保留请求级上下文和关键路径时序,对该路径形成补充。
这正是本节的主要实现目标:先构建最小可用的埋点层,之后再以更丰富的工具替换其中的各个部分。一旦服务能够暴露指标、输出结构化日志并记录 span,将 SLO 违反与具体的运维操作关联起来就会容易得多。埋点就位后,我们现在就可以从系统遥测转向真实流量下的在线模型质量,包括延迟标签、领先指标和分群感知监控。
真实世界中的在线质量
在离线评估中,ground truth(真实标签)可以即时获得。而在生产环境中,情况很少如此。点击标签可能几分钟后才到达,拒付可能几周后才浮现,人工审核可能需要数天。然而,服务仍然必须在今天就保持在质量 SLO 之内。这就是在线质量监控的核心挑战:即使确切的标签稍后才会到达,现在也必须做出决策。在实践中,解决方案包含三个部分:将标签视为异步流,监控实时变动的领先指标,并对一切进行分群,让局部问题在演变为全局问题之前就显现出来。接下来的几个小节将这一挑战转化为一种可操作的模式。首先,它们展示如何将延迟标签排除在热路径之外并在之后进行对账;随后介绍实时领先指标和基于分群的视图,帮助在滞后的 ground truth 到达之前发现新出现的质量问题。
延迟标签:稍后关联,而非在热路径中
延迟标签是指正确结果在预测已经对外服务之后才到达。这在点击率系统、欺诈检测、物流、支持工单分类和许多安全流程中都很常见。关键在于保持请求路径的快速,同时保留足够的信息以供日后对账标签:一个稳定的标识符、预测分数、所做的决策,以及分群的紧凑描述(如地区、设备和应用版本)。
第一步是定义一个紧凑的分群键和一个轻量级的预测记录,该记录可以在推理时存储,而不会拖慢请求路径。
// quality.hpp
#pragma once
#include <cmath>
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
struct CohortKey {
std::string region, device, app;
bool operator==(const CohortKey& o) const {
return region == o.region && device == o.device && app == o.app;
}
};
struct CohortKeyHash {
size_t operator()(const CohortKey& k) const noexcept {
std::hash<std::string> H;
return H(k.region) ^ (H(k.device) << 1) ^ (H(k.app) << 2);
}
};
// Record persisted or queued at prediction time.
struct PredRecord {
std::string id; // stable request or event id (not PII)
CohortKey cohort;
float score; // predicted probability or confidence
int decision; // thresholded action, e.g., 0/1
int64_t ts_ms; // event time
};
这条记录刻意保持小巧。它包含足够的信息来支持后续对账,而无需存储原始载荷或个人身份识别数据。一旦预测可以按分群进行键控,下一步就是构建一个滚动摘要,用于累积带标签的结果并支持校准和概率误差指标。
struct RollingQuality {
uint64_t n = 0, n_pos = 0;
double brier_sum = 0.0;
// Reliability buckets for ECE (10 bins)
uint64_t bins[10]{}, bin_pos[10]{};
void observe(float score, int label) {
n++;
if (label) n_pos++;
double d = double(score) - double(label);
brier_sum += d * d;
int b = std::min(9, std::max(0, int(score * 10)));
bins[b]++;
if (label) bin_pos[b]++;
}
double ece() const {
if (n == 0) return 0.0;
double e = 0.0;
for (int b = 0; b < 10; ++b) {
if (!bins[b]) continue;
double conf = (b + 0.5) / 10.0;
double acc = double(bin_pos[b]) / double(bins[b]);
e += (double(bins[b]) / double(n)) * std::abs(acc - conf);
}
return e;
}
double brier() const {
return n ? brier_sum / double(n) : 0.0;
}
};
这个滚动摘要提供了概率质量的两个有用视角:用于校准的 ECE,以及用于整体概率误差的 Brier 分数。延迟标签路径的最后一部分是一个小型关联器(joiner),它临时存储预测记录,并在标签稍后到达时进行对账。
class DelayedLabelJoiner {
public:
void on_prediction(const PredRecord& pr) {
std::lock_guard<std::mutex> lk(mu_);
preds_[pr.id] = pr;
prune();
}
void on_label(const std::string& id, int label) {
std::lock_guard<std::mutex> lk(mu_);
auto it = preds_.find(id);
if (it == preds_.end()) return; // too old or missing
const PredRecord& pr = it->second;
quality_[pr.cohort].observe(pr.score, label);
preds_.erase(it);
}
RollingQuality get(const CohortKey& k) const {
std::lock_guard<std::mutex> lk(mu_);
auto it = quality_.find(k);
return (it == quality_.end()) ? RollingQuality{} : it->second;
}
private:
void prune() {
// Optional: drop stale predictions by TTL to bound memory.
}
mutable std::mutex mu_;
std::unordered_map<std::string, PredRecord> preds_;
std::unordered_map<CohortKey, RollingQuality, CohortKeyHash> quality_;
};
在实践中,预测记录通常会写入持久化队列,关联器则在 sidecar 或后台工作进程中运行。重点在于架构层面:预测时延保持较低,而精确的质量指标在标签到达后才更新,如图 12.9 所示。
图 12.9:延迟标签时间线
预测在时刻 立即对外服务,而标签在较晚的时刻 才到达。后台关联器对两者进行对账,并更新分群级别的校准和误差指标,而不会触及请求路径。
图 12.9 展示了延迟标签如何在预测时间之后进行对账。下一步是用实时信号补充这一较慢的反馈回路,以便在标签到达之前就能预警质量变化。
领先指标:无需标签的质量信号
在标签延迟期间,服务仍然需要与质量相关的实时信号。有三类信号尤其有用:不确定性度量、冠军-挑战者(champion-challenger)分歧,以及弃权(abstention)或覆盖(override)率。
一个有用的无标签信号家族来自不确定性本身。如果模型输出 logits 或概率,那么熵(entropy)和 top-2 间隔等简单函数可以提供廉价的置信度代理。
#include <algorithm>
#include <cmath>
#include <vector>
inline float softmax_entropy(const std::vector<float>& logits) {
float m = *std::max_element(logits.begin(), logits.end());
double Z = 0.0;
std::vector<double> p(logits.size());
for (size_t i = 0; i < logits.size(); ++i) {
p[i] = std::exp(double(logits[i] - m));
Z += p[i];
}
double H = 0.0;
for (double& pi : p) {
pi /= Z;
if (pi > 0) H -= pi * std::log(pi + 1e-12);
}
return float(H);
第二个置信度代理是前两个概率之间的差距,随着模型置信度降低,该差距会缩小。
inline float top2_margin(const std::vector<float>& probs) {
float a = 0.0f, b = 0.0f;
for (float v : probs) {
if (v > a) {
b = a;
a = v;
} else if (v > b) {
b = v;
}
}
return a - b; // smaller means less certain
}
另一个领先指标来自在线模型与影子挑战者的对比。一个简单的分歧计数器就足以跟踪两个模型是否开始出现分歧。
#include <atomic>
struct Disagreement {
std::atomic<uint64_t> n{0}, diff{0};
void observe(int y_hat, int y_hat_shadow) {
n.fetch_add(1, std::memory_order_relaxed);
if (y_hat != y_hat_shadow) {
diff.fetch_add(1, std::memory_order_relaxed);
}
}
double rate() const {
uint64_t N = n.load(), D = diff.load();
return N ? double(D) / double(N) : 0.0;
}
};
如果将低置信度案例路由到人工复核或更安全的基于规则的兜底方案,弃权率或覆盖(override)率就会成为另一个有用的领先指标。突然的上升往往指向上游数据问题、阈值漂移或概念变化。
这些信号应当始终按分组(cohort)进行监控。全局熵均值很容易掩盖某类设备或某个应用版本已开始表现异常的事实。
图 12.10:按分组划分的领先指标
(a) 各应用版本的 Softmax 熵中位数;(b) 各区域的主模型–挑战者分歧;(c) 各设备的弃权率。这里的尖峰通常先于带标签准确率下降出现。
图 12.10 展示了领先指标如何在标注指标恶化之前提供早期预警。某个应用版本的熵上升、某个区域的分歧激增、某个设备分组的弃权增加,而其余分组保持相对稳定。关键在于并非每个信号都在所有地方变化,而是分组感知的视图能让局部回归及早显形,趁它们尚未被全局均值稀释、或被延迟标签确认之前。无标签指标就位后,下一步是按分组组织这些指标,使局部问题保持局部可见。
分组(Cohorting):在问题变大之前将其化小
分组(Cohorting) 是指按区域、设备类别、应用版本或输入形状等稳定标签对事件进行分组,并为每个分组维护指标。这不是可选项。当某个分组在悄然劣化时,单一的全局延迟或 ECE 数值仍可能看起来健康。
为了让这些指标可操作,应当按分组而不是仅按全局均值进行跟踪。下面的结构为每个组维护一份紧凑的滑动摘要。
// cohort_quality.hpp
#pragma once
#include "quality.hpp"
#include <mutex>
#include <unordered_map>
struct CohortLeading {
double ent_ewma = 0.0;
double margin_ewma = 0.0;
double disagree_ewma = 0.0;
double abstain_ewma = 0.0;
bool inited = false;
void update(double ent,
double margin,
double disagree,
double abstain,
double alpha = 0.2) {
if (!inited) {
ent_ewma = ent;
margin_ewma = margin;
disagree_ewma = disagree;
abstain_ewma = abstain;
inited = true;
return;
}
ent_ewma = (1 - alpha) * ent_ewma + alpha * ent;
margin_ewma = (1 - alpha) * margin_ewma + alpha * margin;
disagree_ewma = (1 - alpha) * disagree_ewma + alpha * disagree;
abstain_ewma = (1 - alpha) * abstain_ewma + alpha * abstain;
}
};
接着,一个轻量级分组监控器可以在请求处理和影子评估期间更新这些摘要,同时沿用延迟标签连接器所用的同一组分组定义。
class CohortMonitor {
public:
void on_request(const CohortKey& k,
double entropy,
double margin,
bool abstain) {
std::lock_guard<std::mutex> lk(mu_);
auto& c = lead_[k];
c.update(entropy, margin, /*disagree=*/0.0, abstain ? 1.0 : 0.0);
}
void on_shadow_pair(const CohortKey& k,
int y_hat,
int y_hat_shadow) {
std::lock_guard<std::mutex> lk(mu_);
auto& c = lead_[k];
double disagree = (y_hat != y_hat_shadow) ? 1.0 : 0.0;
c.update(c.ent_ewma, c.margin_ewma, disagree, c.abstain_ewma);
}
CohortLeading get(const CohortKey& k) const {
std::lock_guard<std::mutex> lk(mu_);
auto it = lead_.find(k);
return (it == lead_.end()) ? CohortLeading{} : it->second;
}
private:
mutable std::mutex mu_;
std::unordered_map<CohortKey, CohortLeading, CohortKeyHash> lead_;
};
DelayedLabelJoiner 与 CohortMonitor 共同提供在线质量的两个视角:当下看领先指标,之后看精确的标注指标,两者都按同一分组结构进行拆分。
从信号到行动
信号只有能引导相应规模的应对才有意义。某个分组的熵和分歧上升,而标注 ECE 仍在 SLO 以内时,可能需要更严格的弃权策略,或对挑战者模型进行影子评估。某个分组的 ECE 随后突破 SLO,则可能需要重新校准、阈值更新或针对性重训练。某个应用构建的弃权率突然上升,可能表明数据契约或预处理存在问题,而不是模型漂移。
由于一切都是以分组为感知的,应对措施可以保持局部化,而不必强制全局回滚或重训练。
将在线质量接入你已有的遥测系统
本章前面用过的指标与日志基础设施,同样可以暴露这些在线质量信号。保持标签集有界,例如区域限定为 {NA, EU, APAC},设备限定为 {ios, android, web},应用版本采用主次版本粒度,而不是完整构建字符串。
质量指标示例可能包括:
quality_ece{region="EU",device="ios",app="4.9"} 0.021quality_brier{region="EU",device="ios",app="4.9"} 0.067lead_entropy_ewma{region="EU",device="ios",app="4.9"} 0.45lead_disagreement_ewma{region="EU",device="ios",app="4.9"} 0.08abstain_rate_ewma{region="EU",device="ios",app="4.9"} 0.12
同样的分组感知信号也可以通过结构化日志呈现,这便于在事件响应期间检查回归。
{
"cohort": {
"region": "EU",
"device": "ios",
"app": "4.9"
},
"score": 0.74,
"entropy": 0.39,
"margin": 0.22,
"abstain": false
}
这让事件分析容易得多:哪个分组最先变动,哪个信号最先变动,以及传播速度有多快?
在线质量工作流引入了本章后续反复出现的几个概念。以下术语总结了用于描述延迟反馈、领先指标和分组感知质量监控的核心词汇。
- 延迟标签(Delayed labels): 真实值在预测已对外服务之后才到达
- 领先指标(Leading indicators): 不确定性、分歧或弃权等无标签信号,它们实时变动并与质量相关
- 分组(Cohorting): 按区域、设备或应用版本等标签分组,使局部回归不会消失在全局均值中
- 校准 / ECE(Calibration / ECE): 预测概率与观测结果的一致程度;ECE 用分桶可靠性近似这一差距
- Brier 分数(Brier score): 预测概率与二分类结果之间的均方误差;越低越好
当精确标签有所延迟、但运维决策仍需实时做出时,这些术语为在线质量的推理提供了基本词汇。
图 12.11:按分组划分的可靠性图(区域 × 设备,应用版本 4.9)
上图每个面板都在 x 轴上绘制预测置信度,在 y 轴上绘制观测准确率。一个完美校准的模型会完全沿着对角线分布——置信度 0.8 的预测应有 80% 的准确率。曲线位于对角线上方表示置信不足(模型实际比声称的更准确);曲线位于对角线下方表示过度自信(模型声称的确定性高于实际水平)。曲线与对角线之间的阴影间隙正是 ECE 所衡量的内容:所有置信区间内这些间隙的加权平均值。六个群组中有五个处于健康或临界状态。欧盟 Android 群组(底部中间,红色)是离群值——其曲线在高置信度区间内明显偏离对角线,ECE 突破了 0.06 的阈值,并被标记为需要重新校准。这种模式——一个群组质量下降而全局平均值仍在正常范围内——正是群组级监控旨在捕获的故障模式。
按这种方式处理,在线质量变得可观测而非神秘难测。预测可以立即提供,精确标签可以稍后回流,领先指标可以实时监控,问题可以被控制在它们最初产生的群组范围内。有了在线质量层之后,下一节将讨论这些信号如何驱动告警、漂移预算和运维响应。
从信号到行动
没有行动的观测只是一块仪表盘。收集 SLI(如延迟、可用性、校准、漂移和资源余量)的意义在于决定何时呼叫人工介入、何时应用小型运行时调整、以及何时将问题移交给较慢的工作流(如重新校准、调试或重新训练)。接下来的小节将展示如何把前面介绍的指标转化为具体策略:不会抖动的告警、让变化可见的漂移预算,以及将遥测连接到安全运维响应的轻量级控制钩子。
从 SLI 到策略
告警应当表示持续的合约违约,而不是短暂的统计波动。在实践中,这意味着要在滚动窗口上评估指标,将指标与 SLO 或预算进行比较,要求持续最短时长,并加入回滞机制,使告警只在信号回到舒适区间之后才解除。
以下示例展示了策略层的具体形态。服务导出底层指标,告警系统评估规则。
# alerts.yml
groups:
- name: inference-slo
rules:
- alert: P95LatencySLOViolated
expr: histogram_quantile(0.95, sum(rate(req_latency_ms_bucket[5m])) by (le,endpoint,cohort)) > 120
for: 10m
labels: {severity: page, team: inference}
annotations:
summary: "p95 latency breach on {{ $labels.endpoint }} ({{ $labels.cohort }})"
detail: "p95={{ $value | printf \"%.1f\" }} ms (>120) for 10m; check batching and queue spans"
- alert: AvailabilityBudgetBurn
expr: sum(rate(http_5xx_total[5m])) by (endpoint) / sum(rate(http_requests_total[5m])) by (endpoint) > 0.001
for: 15m
labels: {severity: page, team: platform}
annotations:
summary: "Availability budget burning on {{ $labels.endpoint }}"
detail: "5xx share >0.1% sustained; inspect recent deploys and error logs"
这两条规则捕捉了最常见的面向用户的故障:延迟过高且持续时间过长,以及可用性消耗错误预算过快。接下来是校准和漂移告警:
- alert: CalibrationDrift
expr: quality_ece{cohort!=""} > 0.03
for: 30m
labels: {severity: ticket, team: ml}
annotations:
summary: "ECE breach in {{ $labels.cohort }}"
detail: "ECE={{ $value | printf \"%.3f\" }} (>0.03); consider recalibration or threshold update"
- alert: FeatureDriftPSI
expr: feature_psi{feature=~"price|country|device"} > 0.2
for: 30m
labels: {severity: ticket, team: data}
annotations:
summary: "Feature drift on {{ $labels.feature }} ({{ $labels.cohort }})"
detail: "PSI > 0.2 for 3 windows; inspect transforms, units, and unseen-category rate"
具体阈值仅供参考,但背后的推理是实用的(图 12.12)。如果 p95 超过 120 ms,服务通常就会让人感觉响应迟钝。如果 ECE 超过 3%,阈值和审核队列可能开始偏离。PSI 达到 0.2 通常被视为有意义的数据分布偏移,但不会因细微的季节性波动而呼叫告警。for 持续时间带来了回滞,使告警代表真实事件而非噪声。
图 12.12:从信号到行动
指标触发告警;告警触发行动。延迟违规呼叫值班人员,校准违规开启重新校准任务,特征漂移违规路由到数据管道负责人。
告警阈值就位后,下一步是跟踪系统已经历的漂移或退化程度。
漂移预算
错误预算量化了可用性 SLO 在给定时间段内允许的不可靠程度。漂移预算对分布变化和校准做同样的事:它把需要采取行动之前允许的变化量明确化。一个简单有效的台账可以按群组跟踪三个有用的量:
- 校准预算:ECE 随时间累积的总量;如果在固定时间窗口内总量超过限制,则需要重新校准或重新训练。
- 协变量预算:特征漂移超过阈值(如 PSI > 0.2)的时间窗口数量。
- 质量预算:延迟标签到达后,超出质量 SLO 的标记超支。
这些不是硬性法规,而是护栏。其目的是让漂移响应变得可预测、可见。一个小型的进程内台账足以表达这一思想:
struct DriftBudget {
double ece_cum = 0.0; // cumulative daily ECE
uint32_t psi_breaches = 0; // number of drift windows above threshold
uint32_t quality_overspend = 0;
void accrue_daily(double ece_day, uint32_t psi_windows_over) {
ece_cum += ece_day;
psi_breaches += psi_windows_over;
}
bool needs_recalibration(double ece_limit = 0.30) const {
return ece_cum > ece_limit;
}
bool needs_retrain(uint32_t psi_limit = 12) const {
return psi_breaches > psi_limit;
}
};
这些值可以导出为普通仪表指标,例如 budget_ece_cum{cohort=...} 和 budget_psi_breaches{cohort=...},并放在与延迟和成本相同的仪表盘上。
预算使系统更易于推理。告警展示当前发生的情况,预算展示在需要更强干预之前还剩多少余地。
将告警连接到行动
告警不应只是向渠道中添加噪声。在实践中,有三类响应。有些是即时的服务端调整,不涉及模型更改,例如降低微批处理延迟、提高并发度或收紧阈值。有些属于调试手册,例如提高追踪详细程度或收集复现包。另一些属于较慢的离线循环,例如重新校准或定向重新训练。
一个有用的模式是让服务端操作保持保守且可逆。服务暴露少量运行时调节旋钮,告警层或运维控制台只直接调整这些旋钮。更重量级的更改保持在服务进程之外。
一个安全的小型模式是在 C++ 服务内部实现一个 ActionRouter,它接收告警系统发送的“指令”(通常通过 webhook),并应用保守、可逆的更改;更重量级的操作则交由外部管道处理。
// action_router.hpp
#pragma once
#include <atomic>
#include <iostream>
#include <string>
struct Tuning {
std::atomic<int> microbatch_delay_ms{8};
std::atomic<int> max_concurrency{4};
std::atomic<float> score_threshold{0.7f};
};
一个小型的运行时调优状态可以保存告警可能需要调整的服务端参数。
第 12 章:监控已部署的模型
这些旋钮通常可以在不改变模型权重或重新部署服务的情况下立即调整。随后,操作路由器(action router)可以通过将告警驱动的命令映射到一小组运行时调整项,以可控的方式应用这些更改。
class ActionRouter {
public:
explicit ActionRouter(Tuning& t) : t_(t) {}
void apply(const std::string& kind, const std::string& payload) {
if (kind == "set_threshold") {
float th = std::stof(payload);
float old = t_.score_threshold.exchange(th);
log("threshold", old, th);
} else if (kind == "set_microbatch_delay") {
int ms = std::stoi(payload);
int old = t_.microbatch_delay_ms.exchange(ms);
log("microbatch_delay_ms", old, ms);
} else if (kind == "set_concurrency") {
int k = std::stoi(payload);
int old = t_.max_concurrency.exchange(k);
log("max_concurrency", old, k);
} else if (kind == "enable_debug_mode") {
debug_mode_ = true;
std::cerr << "[action] debug_mode=on\n";
}
}
private:
template <typename T>
void log(const char* key, T oldv, T newv) {
std::cerr << "[action] " << key << ": " << oldv << " -> " << newv << "\n";
}
Tuning& t_;
bool debug_mode_{false};
};
这种模式让控制回路保持明确且可审计。每次更改都应记录由谁触发、为何触发以及何时发生。下面几个示例可以让这种映射更加具体:
- 某个群体的校准违规: 导出该群体最近的
{score, label}对,离线拟合新的温度参数,编写带有群体标记的校准产物,并让服务加载它。如果 ECE 下降并保持在 SLO 范围内,告警会在无需完整重新训练的情况下消除。 - 某个输入字段的特征漂移违规: 首先将问题转交给数据管道负责人。价格上的 PSI 告警可能表示单位变化、缺失值或 schema 问题,而非模型问题。
- 延迟违规且 span 中排队严重: 首先将问题转交给数据管道负责人。在修改模型代码之前,先减少微批延迟或增加容量。
关键在于比例性:并非每次告警都意味着需要重新训练,也并非每次回退都属于模型的职责范围。
通过护栏触发更慢的循环
重新训练不应是一种自动反射。更安全的推进方式通常是 影子(shadow) | 金丝雀(canary) | 升级(promote),一旦运维或质量 SLO 被违反,就自动回滚。因此,告警或预算违规可能会触发一个外部管道,而不是立即更改线上模型,例如:
POST /pipeline/retrain
{
"model": "clickranker",
"reason": "drift_budget_exhausted",
"cohort": {"region":"EU","device":"ios"},
"window": {"start":"2025-08-01","end":"2025-09-01"},
"constraints": {"p95_ms": 120, "ece": 0.03, "cost_per_1k": 0.12}
}
该管道在服务必须满足的相同运维约束(constraints)下训练候选模型,然后将最佳模型注册为影子(shadow)部署:
POST /control/serve
{
"action": "shadow",
"model_version": "clickranker-2025-09-01-eu-ios",
"traffic_share": 0.0, // no user impact
"collect": ["disagreement","latency","mem"]
}
如果影子遥测持续表现更好,就可以推进到小型金丝雀部署,并在 p95、p99、ECE、服务器错误率(HTTP 5xx)和成本上设置回滚门禁。具体的服务栈因实现而异,但原则是相同的:升级取决于实测行为,而非乐观估计。
图 12.13:影子 | 金丝雀 | 升级
候选模型必须先通过影子阶段(领先指标),再通过金丝雀阶段(完整 SLO),之后才能升级;回滚门禁保护尾部延迟和校准。
当告警指向一个仍无法解释的问题时,更深入的调试流程属于 第 11 章 中描述的运行手册。本章的作用是让这些运行手册变得及时且基于证据。
在生产环境之前测试你的策略
策略应该在需要之前刻意演练。以下两种简单的演练尤其有用:
- 合成漂移注入:在预发布环境中扰动特征分布或重映射某个类别。漂移告警应当触发,校准应当出现波动,并且在标签数据证明有必要之前,不应启动任何重新训练任务。
- 延迟混沌:在服务路径中注入队列延迟。延迟告警应当触发,控制回路应当降低微批延迟或提高并发度,p95 应当回归到 SLO 附近。
如果告警在预期时没有触发,或者在不该触发时反复抖动,应在广泛部署之前修正该策略。
以这种方式处理,服务会变得更容易掌控。告警反映的是持续性的违规而非噪声,预算让漂移在变成意外之前就可见,运行时操作保持小而明确且可逆。延迟、校准和成本之所以能保持在可控范围内,靠的不是英雄主义,而是设计。
综合来看,这些部分共同构成了完整的监控闭环。服务暴露正确的信号,策略层将它们转化为告警和预算,控制路径将持续性违规映射为相称的操作。有了这个框架,本章现在可以通过总结让生产监控行之有效的核心思想来收尾。
总结
在生产环境中,只有当周围的服务在真实流量下保持可观测、可靠且高效时,模型才算成功。本章定义了延迟、质量、吞吐量和利用率的核心 SLI,并展示了如何将它们转化为 C++ 推理服务的实用 SLO。随后展示了如何通过轻量级埋点来暴露这些信号:易于抓取的指标、结构化日志和基于 span 的追踪。这些工具使得持续测量服务行为并在回退出现时定位它们成为可能。由于生产环境中的标签通常会有延迟,本章还介绍了基于延迟标签指标和实时领先指标两者的在线质量监控。通过按群体跟踪这些信号,团队可以及早发现局部回退,而不是依赖可能掩盖这些问题的全局平均值。
本章最后通过告警、漂移预算和小型运行时控制,将可观测性与行动联系起来。这些部分加在一起,让已部署的模型不仅可监控,而且可在运维上被管理。下一章将在此基础上,聚焦当信号超出预期范围时如何响应。它将展示如何系统性地调查回退、隔离可能的原因,并利用来自指标、日志和追踪的证据来指导调试、重新校准和恢复。
问题
以下问题有助于复习本章引入的主要概念。
- 在模型服务的语境下定义 SLI 和 SLO。请各举两个例子。
- 为什么我们要跟踪 p95/p99 延迟,而不是依赖平均值?请提供一个用户体验方面的例子。
- 什么是 首字节/首个 token 时间(TTFB/TTFT)?为什么它对 LLM 端点很重要?
- 区分在线准确率与校准(calibration)。在什么情况下准确率看起来正常但校准却很糟糕?
- 你的可用性 SLO 是 99.9%。在一个 30 天的月份中,这大约允许多少停机时间?
- 说出你会从 C++ 推理服务导出的三种 Prometheus 指标类型,以及每种类型捕获的内容。
- 与临时的 printf 日志相比,结构化日志能为你带来什么?说出你会包含的三个字段。
- 追踪和 span 如何帮助区分计算瓶颈与排队瓶颈?
- 定义协变量(特征)漂移、标签/先验偏移和概念漂移。请为每种提供一个具体例子。
- 你的某个群体的 ECE 突破了 3%,但准确率没有变化。你会首先尝试什么操作,为什么?
- p99 延迟刚刚翻倍,但 GPU 利用率保持平稳且批处理大小缩小了。你会先调整哪个旋钮?
- 解释漂移预算(drift budget)。它与错误预算有何相似之处?你会如何将其落地执行?
延伸阅读
以下参考资料提供了本章涉及的主要主题的额外阅读。
- 站点可靠性工程(谷歌)—— SLI、SLO 与错误预算基础
- OpenTelemetry 规范 —— 分布式系统的追踪模型与 span 语义
- Prometheus:监控系统与 TSDB —— 指标类型、直方图与告警模式
- 论现代神经网络的校准(Guo 等人,ICML 2017)—— 实用的 ECE 与温度缩放
- 群体稳定性指数(PSI)指南(行业白皮书)—— 生产环境中简单且稳健的漂移检测
- 规模下的延迟与尾部(Dean & Barroso)—— 为什么 p95/p99 驱动用户体验
- NVIDIA Nsight / VTune / perf —— 将 span 与实际计算和内存行为关联起来的分析器
答案
准备好后,请将你的回答与下面的简要答案进行比对,以确认你的理解:
- SLI:服务行为的可测量信号(例如 p95 延迟、HTTP 5xx 错误率、ECE、GPU 内存余量)。SLO:你为该 SLI 承诺的目标(例如 p95 ≤ 120 ms、可用性 ≥ 99.9%、ECE ≤ 3%)。
- 平均值会掩盖尾部问题;用户感受到的是离群值。如果自动补全端点的平均值为 40 ms,但 p95 为 180 ms,那么每二十次按键就会有一次“卡顿”,削弱感知速度。
- TTFB/TTFT 是发送第一个字节或令牌所用的时间。对于大语言模型,快速的首个令牌能锚定用户感知;即使总时间很长,及早进行流式输出也能改善用户体验,并减少会话取消次数。
- 准确率是正确率;校准衡量预测概率是否与观测频率匹配。你可能很准确(标签正确),但校准不佳(0.9 的分数并不意味着大约 90% 的正确率),这会破坏阈值设定和复核规模的确定。
- 约 43 分钟(30 天共 43,200 分钟中的 0.1%)
- Counter(例如
http_requests_total)、Gauge(例如gpu_mem_used_bytes)、Histogram(例如req_latency_ms_bucket) - 结构化日志允许机器解析和关联。至少应包含:
request_id、模型/版本、分组(地区/设备/应用版本)、形状/数据类型、latency_ms(或每个 span 的延迟)以及result_status - 追踪每个阶段的时间跨度。如果队列增长而推理保持不变,问题在于准入或批处理;如果推理增长,则是计算问题(模型或回退、精度变化、内核问题)。
- 协变量漂移:输入特征发生变化(例如,新设备组合提高了图像分辨率)。标签/先验偏移:类别频率改变(例如,欺诈率下降一半)。概念漂移:输入到输出的映射改变(例如,新政策重新定义了什么是“垃圾邮件”)。
- 重新校准(温度缩放/Platt 缩放)或调整该分组的阈值;校准可以在不重新训练权重的情况下修复概率质量。
- 提高微批处理的有效性(如果队列截止时间过长并拉高尾部延迟,则缩短它;或者,由于批处理规模缩小了,可略微增加最大延迟以重建批大小)。目标是在修改模型之前恢复健康的批分布。
- 漂移预算量化了在一段时间内你容忍的分布或校准变化量(例如 PSI 窗口 > 0.2 或累积 ECE)。与错误预算类似,超出预算会触发行动(修复流水线、重新校准或重新训练),并在变更推出后重置。
获取本书 PDF 版本及更多资源
扫描二维码(或访问 packtpub.com/unlock)。按名称搜索本书,确认版本,然后按照页面上的步骤操作。
https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/deep-learning-with-c/img/f45df257dfb11cd9b58ac2c5f6935210_545_0.png
https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/deep-learning-with-c/img/f45df257dfb11cd9b58ac2c5f6935210_545_1.png
注意:请保留你的发票。直接从 Packt 购买则无需发票。
13. 深度学习模型中的可解释性与透明性
在某些应用中,仅有一个预测结果是不够的。当模型影响医疗、贷款、招聘或安全决策时,人们需要理解它为何做出该判断,以及应该给予多大程度的信心。这正是可解释性的价值所在。本章将探讨让深度学习模型更易理解的实用方法,使用你可以与 C++ 推理栈一起部署的技术。
我们将沿着实践中会用到的三个轴来构建可解释性:局部 vs. 全局(解释单个决策 vs. 模型整体行为)、事后 vs. 内生(分析已训练模型 vs. 一开始就为可解释性而设计)以及模型无关 vs. 模型特定。模型无关方法如局部可解释模型无关解释(LIME)和SHapley 加法解释(SHAP)可以跨架构使用;而模型特定方法如梯度加权类激活映射(Grad-CAM)则为特定模型族设计,例如卷积神经网络(CNN)。你还将学习在忠实度(解释是否真正反映模型的内部机制)与有用性(对人类读者来说是否可理解、可操作)之间取得平衡。在此过程中,我们还会指出常见陷阱,包括特征相关性、梯度饱和、具有误导性但看似合理的归因,以及基于扰动方法的计算成本。
从工程角度看,我们将尽可能贴近底层。你将使用已经部署的同一套推理后端(如 LibTorch 或 ONNX Runtime)在 C++ 中实现 LIME、SHAP 和 Grad-CAM。我们还会介绍用于伴随预测一起提供解释的 API 设计、用于控制延迟的缓存与批处理策略,以及在用户界面中负责任地呈现解释的方法。
在本章中,我们将涵盖以下主题:
- 理解可解释性在深度学习中的重要性
- 介绍核心技术(LIME、SHAP、Grad-CAM)
- 在 C++ 中实现 LIME 和 SHAP
- 将 Grad-CAM 应用于卷积神经网络
- 处理伦理考量
到本章结束时,你将理解在 C++ 系统中解释深度学习模型的主要方法,包括 LIME、SHAP 和 Grad-CAM。你还将了解如何在真实应用中负责任地使用这些解释,同时关注实际部署与伦理边界。
理解可解释性在深度学习中的重要性
高准确率很重要,但在许多领域仅靠准确率本身并不足够。当模型输出影响医疗、信贷决策、招聘、安全系统或法律程序时,利益相关者不仅仅需要知道做出了一个预测。他们需要理解预测为什么做出,应该对其赋予多大的信心,以及哪些因素可能改变结果。这正是可解释性的作用:将一个复杂函数转化为人类可以评估并据此行动的原因,同时又不过度简化模型的实际行为。
可解释性不是单一的概念或工具。在实践中,可以从三个主要轴来思考它:
- 局部 vs. 全局:我们是在解释某个特定决策,还是模型的整体行为?
- 事后 vs. 内生:我们是事后分析已训练好的模型,还是从一开始就设计可解释的模型?
- 模型无关 vs. 模型特定:我们是使用可跨架构工作的技术(如 LIME 和 SHAP),还是专门针对特定模型族的方法(如用于 CNN 的 Grad-CAM)?
图 13.1:在可解释性轴上比较 LIME、SHAP 与 Grad-CAM
图 13.1 中的雷达图展示了这三种方法在范围、训练策略和模型耦合度上的差异。LIME 主要面向局部且模型无关;SHAP 可同时支持局部和全局解释;Grad-CAM 则是一种用于可视化卷积神经网络(CNN)决策的模型特定方法。
这些区别有助于阐明为什么没有任何一种解释方法适用于所有用例。下一小节将探讨何时需要解释、不同利益相关者对其有何期望,以及可解释性如何在已部署系统中支撑信任、安全与治理。
识别何时需要解释
每当模型的输出对人的权益、安全或责任归属产生重大影响时,解释就变得尤为重要。在医疗保健领域,分诊或诊断模型应提供患者层面的原因(例如显著的测量值或图像区域),并附上一定程度的不确定性提示,以便临床医生核实或推翻建议。在金融领域,诸如贷款利率变更或拒绝贷款等决策通常需要理由代码和文档记录,以支持一致性与复核。在安全关键型系统(如自动驾驶或工业检测)中,解释有助于团队调查故障,并判断问题究竟来自分布偏移、传感器问题还是糟糕的决策边界。在面向公众的产品(如推荐或内容审核系统)中,可解释性还能减少“决策由不透明黑箱做出”的感觉,从而增强用户信任。
了解不同利益相关者的需求
不同的受众需要不同形式的解释、不同的详细程度以及不同的响应时间:
-
临床医生和其他领域专家通常需要基于领域特征的案例级解释。他们可能需要知道哪些测量值、症状或图像区域对预测的贡献最大,还需要校准的风险评分、置信区间,以及提示何时不应信任模型的信号,例如分布外输入、图像质量差,或缺少关键特征或字段。
-
审计人员、合规与风险审查人员通常需要更广泛的文档。这些文档可能包括模型卡片、数据血缘、队列级性能和校准、公平性分析、特征治理,以及与当时使用的确切模型版本、阈值和输入模式相关联的决策日志。
-
最终用户需要更简短清晰且带有补救措施的解释。他们受益于通俗易懂的影响结果的主要因素摘要,并适当提供相关资源,例如哪些行动可以改善未来的结果,或者缺失信息如何影响决策。
-
工程师和运维人员需要准确反映模型行为并支持调试的解释。他们可能会使用显著性图、归因模式、分歧信号和队列摘要来识别脆弱特征、追踪故障模式和检测漂移。对于这类受众,保真度比呈现方式更重要。
将可解释性与信任、安全和监管联系起来
可解释性是在深度学习系统中建立信任的一个组成部分。其他部分包括校准、鲁棒性和补救措施,但解释发挥着特殊作用,因为它允许人们检查、质疑和挑战模型输出。既忠实又可理解的解释使专家更容易验证系统,也使受影响的用户更容易解读其决策。
可解释性也是一种安全机制。归因方法和激活图可以揭示捷径学习、不稳定代理,或当条件变化时模型所依赖特征的转移。与监控(第12章)相结合时,可解释性有助于决定何时优雅降级、触发人工审查或将模型下线。
此外,可解释性支持监管预期和内部治理要求。在许多情况下,组织必须以支持审查和审计的方式记录模型的目的、局限性、输入、结果和决策逻辑。即使法律没有明确要求可解释性,将解释工件作为生产界面的一部分保留下来,也会使审查、风险管理和事件分析变得更加容易。
应用实际设计原则
在实际系统中设计可解释性时,有两条原则尤其重要:
-
解释必须符合延迟和隐私预算。 计算密集型的扰动方法无法在紧张的p95延迟下同步运行;你可能需要在线上使用缓存或近似的归因,在线下生成更完整的报告。解释绝不应泄露敏感字段或导致被利用;在适当的地方应进行脱敏和聚合。
-
一致性优先于精巧。 除非模型行为确实发生变化,否则解释应在小的输入变化下保持稳定,并跨模型版本保持一致。如果近乎相同的输入会导致归因结果大幅跳动,那么它们将无法支持临床或监管用途,并会困扰最终用户。
图13.2(占位图):利益相关者需求矩阵
不同的利益相关者看重不同的解释工件(图13.2)。临床医生、审计人员、最终用户和运维人员需要不同平衡的局部与全局解释、叙述性与数值输出,以及同步与离线报告。有了这个基础,我们现在可以转向实践中使用的主要可解释性方法。下一节将介绍LIME、SHAP和Grad-CAM,并展示它们在目的、假设和部署权衡方面的差异。
核心技术概述(LIME、SHAP、Grad-CAM)
并非所有解释都回答相同的问题。有些方法显示某个特定决策中哪些特征重要;另一些方法强调视觉模型关注的位置;还有一些方法提供跨许多案例汇总的加性归因。在本节中,我们将介绍三种在C++推理栈上开发时广泛使用的技术:LIME、SHAP和梯度加权类激活映射(Grad-CAM)。我们将解释每种方法旨在估计什么、在何处最有用,以及它在保真度、延迟和实现复杂性方面带来了哪些权衡。接下来的各小节将依次深入讨论每种方法,展示其工作原理、依赖的假设,以及在实际系统中使用时需要牢记的实际问题。这些小节将帮助你决定哪种解释方法最适合你的模型、用户和部署约束。
LIME——针对单个预测的局部、模型无关解释
LIME是局部可解释性的一个有用起点,因为它只需对服务栈进行相对较少的改动即可应用于许多不同的模型类型。在本小节中,你将了解LIME旨在估计什么、在实践中如何工作,以及在生产环境中使用它时需要注意哪些权衡。
-
直觉与目标:LIME通过在输入周围的小邻域内近似黑盒模型来解释单个决策。它生成原始样本的小扰动,对这些变体查询模型,根据每个样本与原始点的接近程度对其加权,并拟合一个简单的替代模型——通常是稀疏线性模型。然后,该替代模型的系数被解释为局部特征重要性——即对于这个特定输入、在微小且合理的改变下的重要性。
-
所估计的内容:对于输入x₀,LIME通过求解下式来拟合替代模型g_β:
β̂ = argmin_β ∑_{i=1}^{N} π(x_i, x₀) (f(x_i) − g_β(x_i))² + Ω(β),
其中x_i是扰动样本,f是原始模型,g_β是替代模型,π(x_i, x₀)是一个邻近核函数,用于降低远端样本的权重,而Ω(β)是一个鼓励稀疏性的正则化项。拟合得到的β可解读为f在x₀附近的局部线性近似。
-
具体示例:
-
表格型信用评分:对收入施加±5–10%的抖动,微调余额,切换少量二元标志,将稀有类别掩蔽到“其他”桶中。对于该申请人的拒绝,替代模型可能报告“负债收入比 +0.24,近期逾期 +0.18,工作年限 −0.07”。
-
文本情感:将句子表示为可解释的标记,通过掩码单词或用中性同义词替换来生成扰动,在打分后的变体上拟合替代模型,以对影响性标记进行排序。
-
图像:将图像分割为超像素,随机关闭超像素(用中性色或模糊替换),对每个变体评分,并在二值的开/关掩码上拟合替代模型。(注意,超像素生成本身会增加计算量,并可能影响稳定性。)
-
-
陷阱与护栏:LIME的保真度取决于你的扰动。如果你采样了不可能的状态(如负年龄、不一致的特征组合),你将在不好的邻域上拟合出良好的替代模型。相关特征可能会在彼此之间转移重要性;特征缩放和合理的核宽度很重要。使用固定的随机数种子来稳定结果,限制显示的特征数量,并记录你的扰动策略。在性能方面,LIME需要数十到数百次额外的前向传播;在C++中,你可以对这些查询进行微批量处理,并重用缓冲区,以将p95延迟控制在预算内。
图13.3:LIME在表格型信用风险中的应用。使用抖动特征围绕一位申请人拟合局部替代模型(稀疏线性模型);系数大小对该单一决策的影响因素进行排序。
LIME 提供了一种灵活且与模型无关的方式来解释单个预测,但它在很大程度上依赖于其采样的局部扰动的质量。下一小节将介绍 SHAP,它通过为特征分配具有更强理论基础的可加性贡献,从不同角度处理局部归因问题。
SHAP:具有博弈论保证的可加性归因
当你需要既适用于单个预测、又适用于更大范围群体级汇总的解释时,SHAP 尤其有用。在本小节中,你将了解 SHAP 如何分配特征贡献、它与 LIME 的区别,以及为什么在需要一致性和聚合时它往往是首选。
- 直觉与目标: SHAP 使用合作博弈论中的 Shapley 值 为每个特征分配对预测的贡献。你可以将预测 (f(x)) 视为一个特征联盟的收益,其中每个特征的价值是它在所有可能加入其他特征的方式中的 平均边际贡献。结果是具有若干有用性质的可加性解释:归因之和等于 (f(x) - E[f(X)]),即满足局部准确性;对称特征被平等对待;当模型变化时归因也一致地变化。
理解 SHAP 的一个有用方法是从基线预测开始,然后询问当每个特征加入其他特征的不同联盟时,它对该预测产生了多大改变。对这些边际贡献取平均,便得到每个特征的归因,而这些归因之和会回到相对于基线的模型输出。这就是为什么 SHAP 在数学上根基扎实,并且在实践中易于可视化。
- 被估计的内容: 对于特征 (j),Shapley 值为:
[
\phi_j = \sum_{S \subseteq F \setminus {j}} \frac{|S|!,(|F|-|S|-1)!}{|F|!} \left[ f_{S \cup {j}}(x_{S \cup {j}}) - f_S(x_S) \right],
]
其中 (F) 是特征全集,(S) 是不包含特征 (j) 的子集,(\phi_j) 是特征 (j) 的贡献。该公式表明,每个特征获得的贡献等于其在其余特征所有可能联盟中的平均边际贡献。
精确的 Shapley 值需要对所有 (2^d) 个联盟取平均,这使得除非常小的 (d) 之外很快变得不可行。因此,在实践中需要使用近似方法。KernelSHAP 与模型无关,并且与 LIME 类似,求解加权线性回归,但它使用专门设计的 Shapley 核和二元联盟特征。TreeSHAP 为树集成提供精确值,而 DeepSHAP 在特定层假设下利用网络结构。你还必须选择一个背景数据集 (X_{\text{bg}}) 来定义基线 (E[f(X)])。
图 13.4 通过从理论转向可视化来帮助具体化 SHAP。面板 (a) 展示了联盟视角,其中某个特征的 Shapley 值被计算为其在其他特征子集上的平均边际贡献。面板 (b) 展示了单个预测的局部解释,从基线开始逐步加入特征贡献,直到达到最终输出。面板 (c) 展示了跨多个示例的全局蜂群汇总,说明哪些特征具有最大的总体影响,以及其效应如何在整个数据集中变化。
-
具体示例:
- 欺诈检测:计算某一天流量的 SHAP,然后按群体聚合,可以看到“商户类别”和“距离家庭的距离”在区域 A 中占主导,而“设备风险评分”在区域 B 中占主导。将单案例 SHAP 用于争议处理,将全局 SHAP 汇总用于风险治理。
- 医疗分诊:使用从上个月入院记录中抽取的背景集,SHAP 显示乳酸升高和呼吸频率异常是高危标记的主要驱动因素;临床医生随后可以核对案例级归因和群体级模式。
-
陷阱与护栏: KernelSHAP 通常假设联盟内的特征相互独立,因此严重的多重共线性会使归因产生偏差。常见的缓解方法包括对相关特征进行分组、采用条件变体,或选择能反映部署分布(例如按区域、设备或季节)的背景集。背景选择很重要,因为相对于健康人群与相对于最近 ICU 入院患者解释某个案例会产生不同的基线,从而产生不同的归因,因此应明确记录该选择。KernelSHAP 还需要大量模型评估;在 C++ 批量联盟查询中,应缓存重复掩码、预分配缓冲区并固定内存,以保持尾部延迟可预测。
图 13.4:SHAP 如何分解预测
图 13.4 展示了 SHAP 如何通过将预测分解为可在局部和全局层面解读的特征级贡献来解释模型。对于单个预测,SHAP 显示每个特征如何使模型输出偏离基线;对于许多预测,它揭示哪些特征最重要,以及它们的效应如何在数据集中变化。面板 (a) 展示了特征 (A) 的联盟机制:每个节点代表模型对不同特征子集的输出,(\varphi_A) 是特征 (A) 在所有四个联盟上边际贡献的加权平均值。面板 (b) 将此思想应用于一所房屋。基线 (E[f(X)]) 为 $22,500,即所有 506 所房屋的平均预测值,表示模型在不知道某所房屋的任何具体信息时会预测的值。然后,每个 Shapley 值显示知道该特征的实际值如何使预测偏离该平均值:劳动阶层百分比 将其推高 $2,550,房间数量 增加 $1,250,NOX 浓度 将其拉低 $2,100,最终预测为 $24,200。根据构造,这些贡献之和恰好等于基线与预测之间的差距。面板 (c) 将其扩展到整个数据集:每个点代表一所房屋,颜色表示原始特征值,水平分布同时显示每个特征效应的方向和变化范围——而单个平均重要性条形图会将这些信息压缩成一个数字。
当你需要在同时个体和群体层面起作用的可加性归因时,SHAP 尤其有价值。下一小节转向 Grad-CAM,它通过展示卷积神经网络在做出基于图像的决策时关注的位置,来应对另一种不同的解释需求。
Grad-CAM:展示 CNN 在类别决策中关注的位置
当目标是理解哪些图像区域对卷积神经网络的预测贡献最大时,Grad-CAM 尤其有用。在本小节中,你将了解 Grad-CAM 如何将激活和梯度转换为类别特定的热力图,以及在实际系统中使用它时需要注意哪些实际问题。
- 直觉与目标: Grad-CAM 在卷积网络中定位某一类别的空间证据。它将类别得分 (y^c) 的梯度反向传播到所选的卷积层特征图 (A^k),然后将通道权重 (\alpha_k^c) 计算为全局平均梯度。类别特定的热力图构造如下:
[
L_{\text{Grad-CAM}}^c = \operatorname{ReLU}\left(\sum_k \alpha_k^c A^k\right),
]
随后将所得热力图调整为与原始输入图像分辨率一致。ReLU 移除负面证据,并突出最能提高类别得分的区域。
-
具体示例:
- 胸部 X 光分类器:在预测肺炎时,热力图集中在局灶性不透明区域;临床医生可以将其与放射学发现进行比较。
- 制造检测:对于“裂纹”类别,Grad-CAM 突出显示金属表面上的裂缝区域;热力图漂移到徽标或边缘的误触发揭示了捷径学习。
-
陷阱与护栏: 谨慎选择层(通常是最后一个卷积块):太浅会产生噪声较大的图;太深则会因步幅而变得粗糙。注意梯度饱和(某些激活);Grad-CAM++ 和 SmoothGrad 叠加可以提高特异性。热力图忠实于该层的表示,而非绝对真值;需验证高激活区域是否与领域相关线索一致。对于非卷积架构(ViT),应使用注意力展开或 LRP 风格方法,而非经典 Grad-CAM。
深度学习模型中的可解释性与透明度
图 13.5:CNN 上的 Grad-CAM。叠加在输入上的类别特定热力图显示了最后一个卷积块中驱动“肺炎”决策的区域。
Grad-CAM 通过为基于图像的模型提供视觉解释,补充了 LIME 和 SHAP,而不是为表格或文本输入提供特征归因视图。下一节将在此基础上,展示如何在必须满足实时延迟、可用性和部署约束的 C++ 系统中实现解释方法。
在真实约束下选择与组合方法
方法的选择应与所提出的问题、接收解释的受众以及可用的延迟或计算预算保持一致。如果目标是展示视觉模型在最小开销下聚焦于何处,Grad-CAM 通常是最佳选择。如果需要可聚合到大规模人群和长时间窗口的加性、可审计归因,SHAP 提供了更坚实的基础。如果需要快速、模型无关、且设置最少的单个预测解释,LIME 最容易集成到任何 C++ 推理服务中。
在生产环境中,这些方法通常被组合使用,而不是孤立使用。信贷决策系统可能会向申请人返回一段简短的、LIME 风格的局部叙述,同时向风险和合规部门返回 SHAP 摘要。医学影像系统可能会向临床医生展示 Grad-CAM 热力图,同时对相关的表格特征(如生命体征或化验值)使用 SHAP,以解释预测中非图像部分。方法选择还应与 SLO 挂钩:为在线使用提供缓存或近似的解释可能是合适的,而更丰富、更昂贵的报告可以异步生成,用于审计或审查工作流。为保持可复现性,应固定随机种子,对扰动策略和背景数据集进行版本控制,并记录生成每个解释时使用的确切模型和预处理版本。
解释必须在运维上诚实。它们不应泄露敏感属性;当输入分布外时,它们应优雅地退化;并且在输入发生微小变化时应保持稳定。解释不是装饰——它们是产品界面和问责模型的一部分。在考虑这些约束的前提下选择和实现 LIME、SHAP 和 Grad-CAM,可以交付既对专家有用、对用户可理解、又能在实际延迟预算内实用的解释。
下一节将从方法选择转向实现。它展示了如何将 LIME 和 SHAP 构建到 C++ 流水线中,重点关注扰动设计、加权、代理模型拟合,以及在生产环境中保持解释可用所需的性能权衡。
在 C++ 中实现 LIME 和 SHAP
可以随 C++ 推理服务一起部署的解释必须同时满足三个约束:必须足够准确以有用,足够快以符合延迟预算,并且足够简单以在生产中维护。LIME 和 SHAP 在仔细实现时非常适合这种权衡空间。这两种方法都将生产模型视为黑盒 f(x) → y,但它们在估计内容上有所不同:LIME 在单个预测周围拟合一个可解释模型,而 SHAP 分配加性归因,这些归因总和等于预测差值。在实践中,重要的实现选择是如何扰动输入、如何对邻域加权、如何拟合代理模型,以及如何批量查询,以免解释导致尾延迟爆炸。
从预测函数到解释器
我们从一个简单的假设开始:模型已经可以从 C++ 调用。这足以构建 LIME 和 SHAP,因为这两种方法都不需要访问模型内部权重。
// 一个最小的、模型无关的接口。
struct Model {
// 返回标量分数:类别 c 的概率、logit 或
// 回归输出。
double score(const Eigen::VectorXd& x) const;
// 可选的批处理变体,用于性能优化。
void score_batch(const std::vector<Eigen::VectorXd>& X,
std::vector<double>& out) const;
};
以下所有内容都将 Model 视为不透明。解释器只需要生成输入、对其评分,并在结果上拟合一个小型辅助模型。
LIME:局部代理模型需要可信的扰动
当特征空间和扰动策略被明确定义时,LIME 最容易实现。本小节的目标是展示如何生成邻近样本、如何对它们加权,以及如何拟合一个小型代理模型并将其作为可用解释返回。
LIME 通过对邻近点进行采样并拟合一个微小的、人类可读的模型来解释点 x0。“邻近”是在一个可解释特征空间中定义的:原始、标准化的表格特征;文本的 token 开/关指示符;图像的超像素掩码。选择用户能读回的表征。扰动设计最重要的规则是保持在数据流形上。只采样可能实际出现的值。
- 连续特征: 使用训练缩放器标准化为零均值和单位方差,然后添加小的高斯噪声:(x_j^{(t)} = x_{0j} + \epsilon,\ \epsilon \sim \mathcal{N}(0, \sigma_j^2)),并截断回有效范围
- 分类特征: 从该特征的训练分布中采样,或切换少量高频类别,同时将稀有类别映射到“其他”桶
- 混合类型: 使用 Gower 风格距离,使数值和分类特征的差异按范围缩放;分类特征采用 0/1 匹配,因此邻域加权能公平地处理异构特征
每个扰动样本 (x_i) 获得一个邻近权重 (\pi_i = \exp(-D(x_i, x0)^2 / \sigma^2)),其中 (D) 是邻域距离,(\sigma) 控制拟合的局部程度。如果 (\sigma) 太小,代理模型会变得嘈杂;如果太大,解释就不再是局部的。
LIME 通常拟合一个稀疏线性模型,以便只需显示一短串驱动因素。在 C++ 中有两种常见选择:
- 加权岭回归(L2): 拟合一个稳定的线性模型,然后按幅值保留前 K 个系数。
- 真正的 LASSO(L1): 如果需要精确的零和固有稀疏性,则使用坐标下降求解器。
以下是用 Eigen 求解的紧凑加权岭回归。第一步是定义配置和解释负载。
struct LimeConfig {
int n_samples = 512; // 扰动次数
int top_k = 8; // 显示多少特征
double ridge_lambda = 1e-3; // L2 正则化
double kernel_width = 0.75; // 局部性核
};
struct LimeExplanation {
std::vector<int> feat_index;
std::vector<double> feat_weight; // 带符号的重要性
double intercept;
};
接下来,在标准化空间中采样扰动点,并在单个批次中对它们评分。
std::vector<Eigen::VectorXd> sample_lime_points(
const Eigen::VectorXd& x0,
const Eigen::VectorXd& mu,
const Eigen::VectorXd& sigma,
int n_samples) {
using namespace Eigen;
const int d = x0.size();
std::vector<VectorXd> Xs;
Xs.reserve(n_samples);
std::mt19937 rng(42);
std::normal_distribution<double> N01(0.0, 1.0);
for (int i = 0; i < n_samples; ++i) {
VectorXd z = (x0 - mu).cwiseQuotient(sigma); // 标准化
for (int j = 0; j < d; ++j) z[j] += 0.2 * N01(rng);
VectorXd x = z.cwiseProduct(sigma) + mu; // 反标准化
Xs.push_back(x);
}
return Xs;
}
std::vector<double> score_lime_batch(const Model& model,
const std::vector<Eigen::VectorXd>& Xs) {
std::vector<double> ys(Xs.size());
model.score_batch(Xs, ys);
return ys;
}
然后构建加权设计矩阵并求解岭回归。
Eigen::VectorXd fit_lime_ridge(const std::vector<Eigen::VectorXd>& Xs,
const std::vector<double>& ys,
const Eigen::VectorXd& x0,
const Eigen::VectorXd& mu,
const Eigen::VectorXd& sigma,
double kernel_width,
double ridge_lambda) {
using namespace Eigen;
const int n = static_cast<int>(Xs.size());
const int d = x0.size();
MatrixXd X(n, d + 1);
VectorXd y(n), w(n);
VectorXd z0 = (x0 - mu).cwiseQuotient(sigma);
for (int i = 0; i < n; ++i) {
VectorXd zi = (Xs[i] - mu).cwiseQuotient(sigma);
double dist2 = (zi - z0).squaredNorm();
w[i] = std::exp(-dist2 / (kernel_width * kernel_width));
X(i, 0) = 1.0; // bias
for (int j = 0; j < d; ++j) X(i, j + 1) = Xs[i][j];
y[i] = ys[i];
}
MatrixXd W = w.asDiagonal();
MatrixXd XtWX = X.transpose() * W * X;
for (int j = 1; j <= d; ++j) XtWX(j, j) += ridge_lambda;
return XtWX.ldlt().solve(X.transpose() * W * y);
}
最后,提取前 K 个系数以构建解释对象。
LimeExplanation build_lime_explanation(const Eigen::VectorXd& beta, int top_k) {
struct Item { int j; double w; };
std::vector<Item> items;
for (int j = 1; j < beta.size(); ++j) {
items.push_back({j - 1, beta[j]});
}
std::partial_sort(
items.begin(),
items.begin() + std::min(top_k, static_cast<int>(items.size())),
items.end(),
[](const Item& a, const Item& b) {
return std::fabs(a.w) > std::fabs(b.w);
});
LimeExplanation out;
out.intercept = beta[0];
for (int k = 0; k < std::min(top_k, static_cast<int>(items.size())); ++k) {
out.feat_index.push_back(items[k].j);
out.feat_weight.push_back(items[k].w);
}
return out;
}
这样就得到了稳定的局部线性化;如果你确实需要零系数,可以选择用一个小型坐标下降替换岭回归求解器,以获得真正的 L1 稀疏性。
有了局部替代模型之后,下一步是从邻域解释过渡到加性特征归因。这正是 KernelSHAP 的作用:它用基于联盟的加权替代了局部扰动拟合。
SHAP:KernelSHAP 作为一种实用且与模型无关的估计器
SHAP 从另一个角度处理解释问题。它不是直接在原始特征空间中拟合局部替代模型,而是估计在其余特征的众多可能联盟中,每个特征的平均贡献。
对于规模为 M 的特征集合,特征 j 的 Shapley 值是该特征在其他 M−1 个特征的所有联盟中的平均边际贡献。精确计算是指数级的,因此 KernelSHAP 通过将问题重构为对随机采样联盟进行的加权线性回归,使其变得可处理。
SHAP 相对于基线 E[f(X)] 来解释 f(x),因此你必须选择一个能代表典型服务输入的背景数据集 B = {b^(1), ..., b^(K)}。这一选择很重要:从近期 ICU 患者中抽取的背景数据所得出的基线,与从初级保健就诊中抽取的背景数据不同,因此应仔细记录这一选择。
一个联盟由二进制掩码 z ∈ {0,1}^M 表示,其中每个位指示相应特征来自实例还是背景。掩码后的输入为 x̂(z,b) = z ⊙ x + (1 − z) ⊙ b。
对于每个联盟,KernelSHAP 分配一个 Shapley 核权重 ω(z) = (M−1) / (C(M, |z|) · |z| · (M−|z|)),该权重强调中等规模的联盟。然后它拟合加权线性模型 f(x̂(z,b)) ≈ φ0 + Σ_{j=1}^{M} φ_j z_j,其中拟合得到的 φ_j 值即为 SHAP 归因,φ0 近似于基线。
与 LIME 一样,将实现拆分为小块会很有帮助。先从配置和联盟采样开始。
struct ShapConfig {
int n_coalitions = 2048; // number of sampled masks
int background_K = 64; // number of baseline examples
double ridge_lambda = 1e-6; // numerical stability
unsigned seed = 1337;
};
struct ShapValues {
std::vector<double> phi;
double phi0 = 0.0;
};
inline std::vector<uint8_t> sample_mask(int M, std::mt19937& rng) {
std::uniform_int_distribution<int> size_dist(1, std::max(1, M - 1));
int k = size_dist(rng);
std::vector<uint8_t> z(M, 0);
std::vector<int> idx(M);
std::iota(idx.begin(), idx.end(), 0);
std::shuffle(idx.begin(), idx.end(), rng);
for (int i = 0; i < k; ++i) z[idx[i]] = 1;
return z;
}
接下来,构建设计矩阵 Z 和核权重 ω(z)。
void build_kernelshap_design(int M,
const ShapConfig& cfg,
Eigen::MatrixXd& Z,
Eigen::VectorXd& w,
std::vector<std::vector<uint8_t>>& masks) {
Z.resize(cfg.n_coalitions, M + 1);
w.resize(cfg.n_coalitions);
masks.clear();
masks.reserve(cfg.n_coalitions);
std::mt19937 rng(cfg.seed);
auto comb = [](int n, int k) -> double {
if (k < 0 || k > n) return 0.0;
k = std::min(k, n - k);
double c = 1.0;
for (int i = 1; i <= k; ++i) c = c * (n - k + i) / i;
return c;
};
for (int i = 0; i < cfg.n_coalitions; ++i) {
auto z = sample_mask(M, rng);
int k = std::accumulate(z.begin(), z.end(), 0);
double omega = (M > 1)
? static_cast<double>(M - 1) / (comb(M, k) * k * (M - k))
: 1.0;
masks.push_back(z);
w[i] = std::max(omega, 1e-12);
Z(i, 0) = 1.0;
for (int j = 0; j < M; ++j) Z(i, j + 1) = static_cast<double>(z[j]);
}
}
然后生成掩码输入并对它们进行批量评分。
std::vector<Eigen::VectorXd> build_masked_batch(
const Eigen::VectorXd& x,
const std::vector<Eigen::VectorXd>& background,
const std::vector<std::vector<uint8_t>>& masks) {
std::vector<Eigen::VectorXd> batch;
batch.reserve(masks.size());
for (size_t i = 0; i < masks.size(); ++i) {
const auto& z = masks[i];
const auto& b = background[i % background.size()];
Eigen::VectorXd xmask = x;
for (int j = 0; j < x.size(); ++j) {
if (!z[j]) xmask[j] = b[j];
}
batch.push_back(std::move(xmask));
}
return batch;
}
std::vector<double> score_kernelshap_batch(const Model& model,
const std::vector<Eigen::VectorXd>& batch) {
std::vector<double> y(batch.size());
model.score_batch(batch, y);
return y;
}
最后,求解加权回归并得到归因值。
ShapValues solve_kernelshap(const Eigen::MatrixXd& Z,
const Eigen::VectorXd& w,
const std::vector<double>& y,
double ridge_lambda) {
using namespace Eigen;
VectorXd vy = Map<const VectorXd>(y.data(), static_cast<Eigen::Index>(y.size()));
MatrixXd W = w.asDiagonal();
MatrixXd ZtWZ = Z.transpose() * W * Z;
for (int j = 0; j < Z.cols(); ++j) ZtWZ(j, j) += ridge_lambda;
VectorXd theta = ZtWZ.ldlt().solve(Z.transpose() * W * vy);
ShapValues out;
out.phi0 = theta[0];
out.phi.resize(Z.cols() - 1);
for (int j = 0; j < out.phi.size(); ++j) out.phi[j] = theta[j + 1];
return out;
}
第13章:深度学习模型中的可解释性与透明性
有几个关键杠杆会强烈影响准确性和稳定性:
- 背景集:选择能够适配缓存并代表所服务的分布(按地区、设备、季节或其他重要维度)的K值。
- 采样:按联盟大小分层,使小、中、大掩码都得到代表;固定随机种子以确保可复现性。
- 相关性:高度相关的特征违反了独立性假设;尽可能对特征分组或使用条件变体。
- 合理性检查:通过检查归因加基线是否约等于模型输出来验证局部准确性。
将解释控制在你的SLO之内
解释不应让p95延迟飙升。在实践中,一些工程经验法则比任何单一算法技巧都更重要:
- 批量处理一切:LIME和KernelSHAP都极其消耗查询;因此始终使用
score_batch。保持一个单一的std::vector<Eigen::VectorXd>,并在请求之间重用容量,以避免分配器抖动。 - 预分配并固定:如果桥接到GPU,请固定主机内存并重用设备缓冲区。避免在热路径中为每个解释进行malloc/free。
- 限制工作量:用每个请求的预算约束
n_samples(LIME)和n_coalitions(SHAP);在高负载下自适应降低。为在线界面提供“快速模式”(较少样本),为离线审计提供“完整模式”。 - 在合理的地方缓存:对于SHAP,当相同输入类型反复出现时(例如热门SKU),缓存重复掩码/背景行的模型分数。对于LIME,缓存图像ID的超像素分割结果。
- 稳定输出:固定RNG种子;报告样本数量、核宽度和背景摘要,使同一案例的两次解释可重复。
- 明智地分组:在用户理解的同一特征空间(缩放后、PCA前)中运行解释,并清晰标注特征(单位、类别值)。
- 护栏:检测分布外输入(与背景的距离过大)并优雅降级(警告解释可能不稳定)。
展示什么(以及不展示什么)
呈现方式与估计结果几乎同等重要。对于LIME,展示带有符号和单位的前K个系数,以及代理模型的局部R²,以便读者了解线性模型的拟合程度。对于SHAP,显示从基线\(\phi_0\)开始、逐项加上\(\phi_i\)直至达到\(f(x)\)的瀑布图;跨群体时,显示汇总图(条形图或蜂群图),以揭示哪些特征在全局上驱动决策。避免泄露敏感属性,并警惕代理特征;解释是产品隐私面的一部分。
排查保真性问题
当解释看起来不合理时,首先检查输入到解释器的内容,而非模型本身。如果LIME在不同运行之间给出不同结果,则核宽度可能过大或过小,或者扰动可能已经偏离数据流形。收紧扰动范围,将其限制在有效范围内,并标准化距离。如果SHAP与领域直觉不符,请重新检查背景集并检查多重共线性。对冗余特征进行分组或移除,或者采用可用的条件掩码策略。始终验证可加性:
\(\phi_0 + \sum_i \phi_i \approx f(x)\)
当解释与留出的反事实测试(如对收入或剂量的受控变化)相冲突时,请将反事实视为更强有力的现实检验。
归根结底,LIME和KernelSHAP在C++技术栈中是可行的,但前提是要严格处理扰动真实性、邻域加权、数值稳定性和批处理。如果做得好,它们能产生足够快以用于生产、且足够可靠以供临床医生、审计人员和最终用户使用的解释。
本节聚焦于通过扰动和加性归因工作的表格型和文本型解释方法。下一节将转向视觉模型,展示Grad-CAM如何利用激活和梯度来突出驱动卷积神经网络决策的图像区域。
在CNN中使用Grad-CAM
Grad-CAM回答单个预测的局部问题:最后一个卷积模块内部的哪些图像区域对某个类别的分数影响最大?该方法很紧凑:
-
进行前向传播,并从目标卷积层捕获激活张量 \(A^{C \times H \times W}\)
-
将标量类别分数 \(y^c\) 反向传播到该层,以获得梯度 \(\partial y^c / \partial A\);
-
计算通道权重 \(\alpha_k^c\) 作为这些梯度的空间平均值
-
形成粗略的显著图:
\(M = \left(\sum_k \alpha_k^c A^k\right)\);
-
调整生成的热图 \(M\) 的大小,使其与原始输入图像的分辨率匹配,然后将其叠加在图像上。
下面是一个自包含的演示,它在小型CNN中暴露最后的卷积激活,为所选类别计算Grad-CAM,并渲染叠加图。同样的步骤适用于真实的主干网络(例如TorchScript化的ResNet)——你只需访问目标模块的激活张量。
构建暴露目标卷积特征的最小CNN
首要要求是能够访问目标卷积模块的激活张量。在LibTorch中,一种简单的方法是在前向传播期间保存该张量并调用retain_grad(),以便在反向传播后其梯度仍然可用。
// gradcam_demo.cpp
#include <torch/torch.h>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <string>
// ----- Small CNN with an exposed "last conv" activation -----
struct SmallCNN : torch::nn::Module {
SmallCNN(int num_classes=2)
: conv1(torch::nn::Conv2dOptions(3, 16, 3).padding(1)),
conv2(torch::nn::Conv2dOptions(16, 32, 3).padding(1)),
fc(32, num_classes) {
register_module("conv1", conv1);
register_module("conv2", conv2);
register_module("fc", fc);
}
torch::Tensor last_conv_activation; // [N, 32, H, W]
torch::Tensor forward(torch::Tensor x) {
x = torch::relu(conv1->forward(x));
x = torch::relu(conv2->forward(x));
last_conv_activation = x; // capture activations
last_conv_activation.retain_grad(); // keep grads for non-leaf
x = torch::adaptive_avg_pool2d(x, {1,1});
x = x.view({x.size(0), -1});
return fc->forward(x); // logits
}
torch::nn::Conv2d conv1{nullptr}, conv2{nullptr};
torch::nn::Linear fc{nullptr};
};
这个小模型使机制一目了然:最后的卷积激活在forward()中被捕获,然后在Grad-CAM计算梯度和热图时被重用。
使用OpenCV辅助函数进行图像到张量的转换和叠加
在计算Grad-CAM之前,最好将两个工具步骤分离出来:将图像转换为可供模型消费的张量,以及将生成的热图转换为可检查或保存的叠加图。
// Load image, resize, convert to [1,3,H,W] float in [0,1]
torch::Tensor load_image_tensor(const std::string& path, int H=224, int W=224) {
cv::Mat bgr = cv::imread(path, cv::IMREAD_COLOR);
if (bgr.empty()) throw std::runtime_error("Failed to read image: " + path);
cv::Mat rgb; cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB);
cv::Mat resized; cv::resize(rgb, resized, cv::Size(W, H));
resized.convertTo(resized, CV_32F, 1.0/255.0);
auto t = torch::from_blob(resized.data, {H, W, 3}, torch::kFloat32).clone();
t = t.permute({2,0,1}).unsqueeze(0); // [1,3,H,W]
return t;
}
// Make a color overlay (JET) from a [H,W] heatmap (0..1) onto an RGB base (0..1)
cv::Mat overlay_heatmap(const torch::Tensor& heat01, const cv::Mat& base_
第13章:深度学习模型中的可解释性与透明度
cv::Mat overlay_heatmap(torch::Tensor heat01, const cv::Mat& base_rgb, double alpha=0.5) {
torch::Tensor h = heat01.clamp(0, 1).mul(255).to(torch::kU8).contiguous();
cv::Mat heat(h.size(0), h.size(1), CV_8U, h.data_ptr<uint8_t>());
cv::Mat heat_color;
cv::applyColorMap(heat, heat_color, cv::COLORMAP_JET);
cv::Mat base_u8;
base_rgb.convertTo(base_u8, CV_8UC3, 255.0);
cv::Mat overlay;
cv::addWeighted(heat_color, alpha, base_u8, 1.0 - alpha, 0.0, overlay);
return overlay; // BGR 8-bit
}
这些辅助函数使 Grad-CAM 的核心专注于解释逻辑,而非图像处理细节。
从激活与梯度计算 Grad-CAM 图
一旦模型能够暴露其最后一个卷积层的激活,Grad-CAM 计算本身就非常直接。执行一次前向传播,仅对目标类别分数进行反向传播,对梯度在空间上求平均以获得通道权重,然后对激活进行加权求和。应用 ReLU 后,对结果进行归一化,并调整到输入图像大小。
torch::Tensor gradcam_map(SmallCNN& model,
torch::Tensor input, // [1,3,H,W]
int target_class, // class index
torch::Device device) // cpu / cuda:0
{
model.to(device);
input = input.to(device);
// Forward with autograd enabled (we need grads wrt last_conv_activation)
model.eval();
auto logits = model.forward(input); // [1,C]
auto score = logits.index({0, target_class}); // scalar
// Backprop only this score
score.backward(); // populates last_conv_activation.grad()
// Fetch activations and grads for the last conv block
auto A = model.last_conv_activation.detach(); // [1,K,Hc,Wc]
auto dAdy = model.last_conv_activation.grad().detach(); // [1,K,Hc,Wc]
auto device_cpu = torch::kCPU;
// Channel weights α_k = mean_{h,w} grad[k,h,w]
auto weights = dAdy.mean({2,3}); // [1,K]
// Weighted sum over channels → [Hc,Wc]
auto cam = torch::relu(torch::sum(A * weights.unsqueeze(-1).unsqueeze(-1), 1)); // [1,Hc,Wc]
cam = cam.squeeze(0);
// Normalize to [0,1]
auto cam_min = std::get<0>(cam.min({}));
auto cam_max = std::get<0>(cam.max({}));
cam = (cam - cam_min) / (cam_max - cam_min + 1e-8);
// Upsample to input size
auto H = input.size(2), W = input.size(3);
cam = torch::nn::functional::interpolate(cam.unsqueeze(0).unsqueeze(0),
torch::nn::functional::InterpolateFuncOptions()
.size(std::vector<int64_t>{H, W})
.mode(torch::kBilinear)
.align_corners(false))
.squeeze(); // [H,W]
return cam.to(device_cpu);
}
这个函数是方法的核心。它精确展示了激活和梯度如何组合成类别特定的解释图。
运行端到端演示
模型和辅助例程就位后,完整演示便易于理解:加载图像、计算 Grad-CAM 热力图、渲染叠加层并保存。
int main(int argc, char** argv) {
try {
if (argc < 3) {
std::cerr << "Usage: " << argv[0] << " <image_path> <target_class>\n";
return 1;
}
std::string img_path = argv[1];
int target_class = std::stoi(argv[2]);
torch::Device device(torch::cuda::is_available() ? torch::kCUDA : torch::kCPU);
// Load image and keep an OpenCV copy for overlay
cv::Mat bgr = cv::imread(img_path, cv::IMREAD_COLOR);
if (bgr.empty()) throw std::runtime_error("Failed to read: " + img_path);
cv::Mat rgb;
cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB);
cv::Mat rgb_resized;
cv::resize(rgb, rgb_resized, cv::Size(224, 224));
cv::Mat rgb_f;
rgb_resized.convertTo(rgb_f, CV_32F, 1.0/255.0);
auto x = load_image_tensor(img_path, 224, 224);
// Model (random weights for demo; replace with a trained TorchScript or LibTorch model)
SmallCNN net(5); // pretend 5 classes
net.to(device);
// Produce Grad-CAM heatmap
auto heat = gradcam_map(net, x, target_class, device); // [H,W] on CPU
// Render overlay and save
cv::Mat overlay = overlay_heatmap(heat, rgb_f, 0.5);
cv::imwrite("gradcam_overlay.png", overlay);
std::cout << "Saved: gradcam_overlay.png\n";
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 2;
}
return 0;
}
构建 (Linux/macOS)
# Set TORCH to your LibTorch root
c++ -std=c++17 gradcam_demo.cpp -I"$TORCH/include" -I"$TORCH/include/torch/csrc/api/include" \
-L"$TORCH/lib" -Wl,-rpath,"$TORCH/lib" -ltorch -lc10 \
`pkg-config --cflags --libs opencv4` -O2 -o gradcam_demo
./gradcam_demo cat.jpg 3
为简单起见,示例使用随机权重,但集成模式与训练后的网络相同。
在真实网络上应用 Grad-CAM
玩具示例清晰地展示了机制,但生产模型会引入一些实际考量。
- 目标层选择:选择全局池化之前的最后一个卷积块——其特征仍具有空间结构。对于 TorchScript 化的主干网络,暴露该激活(例如,通过拆分模块或添加一个小的包装器),或在原生 LibTorch 模型中注册一个前向钩子以捕获张量。
- 类别分数选择:对于分类任务,反向传播类别 c 的 logit(softmax 之前)。对于多标签任务,对感兴趣的类别的 logit 求和。对于回归任务,直接反向传播标量输出。
- 评估/推理设置:使用
model.eval()冻结批量归一化统计量并禁用 dropout 随机性;仅为你需要的张量(激活和类别分数)保持 autograd 开启,以限制开销。 - 性能:为 Grad-CAM 添加一次反向传播;在 GPU 上,常见主干网络通常只需 2–10 毫秒。如果离线批量生成热力图,可批量处理图像;在线路径中,用功能开关控制。
- 缩放与颜色:在颜色映射之前,始终将 CAM 按图像归一化到 [0,1]。如果依赖跨图像的定量比较,请固定全局归一化策略并记录下来。
- 局限性:Grad-CAM 是类别区分性和粗粒度(特征图分辨率)的:它突出显示类别证据所在之处,而非特征空间中为什么。当需要在输入维度级别进行归因时(前一节),请与 SHAP/LIME 结合用于表格或文本特征。
有了这些组件,Grad-CAM 就成为在 C++ 中部署的视觉模型的一种实用解释方法。它为产品团队、审计人员和最终用户提供了关于 CNN 关注位置的快速视觉提示,同时与已经用于预测的推理栈保持一致。
下一节将从实现机制转向负责任的使用。重点讨论伦理考量:传达不确定性、避免误导性归因、记录已知故障模式,以及决定何时人工审查应覆盖自动化解释。
伦理考量
解释不仅仅是一个用户界面功能。它们影响决策、责任和信任。一个过度夸大置信度、不准确地归因重要性或隐藏已知脆弱性的解释,可能比没有解释更糟。在受监管的领域——信贷、招聘、医疗保健——这些问题决定了系统是否可部署。本节概述了 C++ 推理系统中负责任可解释性的四大支柱:传达不确定性、避免误导性归因、记录已知故障模式,以及建立带可审计痕迹的人工覆盖策略。
传达不确定性而不制造虚假精确
每次预测都带有不确定性,包括来自世界中不可避免噪声的偶然不确定性(aleatoric uncertainty),以及来自有限、有偏或偏移训练数据的认知不确定性(epistemic uncertainty)。诚实地传达这种不确定性有助于防止过度依赖,并为下游用户提供更好的行动依据。
- 优先使用校准概率而非原始分数。当 0.82 的分数表现得像“约 82% 正确”时,它比仅仅是未校准的输出有用得多。在验证集上使用温度缩放或等渗回归,并在生产环境中监控期望校准误差(ECE)。在接口和 API 中,同时暴露类别标签和校准概率,或至少指明分数是否已校准。
- 在适用处提供范围或置信区间。对于回归模型,预测区间(如保形区间)比仅有点估计更有信息量。对于分类器,熵或 top-1 与 top-2 的间隔可以作为紧凑的不确定性信号。
- 明确标记“未知/弃权”。当样本分布外(OOD)或不确定性超过策略阈值时,返回弃权结果并转交人工审查。这对于高成本或安全关键的决策尤为重要。“我不知道”是一个特性,而不是缺陷。
- 用通俗语言解释不确定性的含义。避免暗示必然性或不当预测的措辞。例如:“模型基于与过去案例相似的模式估计概率为 0.73(±0.08);由于近期数据与训练示例不同,不确定性更高。”
第13章:深度学习模型中的可解释性与透明度
在信贷承销示例中,对于处于边缘的申请人,系统可能返回“批准:0.58 ± 0.06(已校准)。置信度适中;缺少收入验证。”如果策略将任何高于 ±0.07 的不确定性视为高风险,则应将案例转交人工裁决。
避免误导性归因
LIME、SHAP 和 Grad-CAM 是局部、近似的解释方法。它们不是因果关系的证明。如果使用不当,它们可能助长错误的叙述,或泄露足够多的细节让人钻系统的空子。
-
明确范围与局限:LIME 和 KernelSHAP 假设实例周围局部线性,并依赖扰动假设;SHAP 值估计的是在所选背景分布下的边际贡献,而不是“如果我改变一件事而保持其他一切不变会怎样”。Grad-CAM 显示网络关注的位置,而非特征空间中的原因。请在文档和工具提示中明确说明这些局限。
-
稳定输出并做健全性检查:当模型或标签被随机化时,解释应当发生变化;在无害的输入对称性下,解释应保持合理稳定。运行离线健全性检查,例如验证随机化模型权重会破坏归因模式。
-
保持可视化一致性:使用固定的颜色映射、稳定的比例和明确的单位,如“概率点”。避免高度饱和的可视化,以免暗示超出方法所支持的精度。对于表格型 SHAP,在中性基线附近显示 top-k 带符号贡献。对于 Grad-CAM,使用一致的 alpha 混合,并在适当时包含比例参考。
-
防止泄漏与博弈:暴露受保护属性、狭窄阈值或精确决策规则的解释可能招致操纵。在面向外部的场景中,给出基于策略的粗略原因,如“还款历史较短”或“使用率较高”,而不是原始权重,并在需要时对解释访问进行速率限制。
对于欺诈检测,在内部,SHAP 揭示“交易速率”和“IP 信誉”主导了高风险决策。在外部,为降低博弈风险,系统可能返回:“决策基于行为异常和设备完整性;请联系支持。”
记录已知失败模式
每个模型都有难以应对的场景。明确这些场景有助于操作人员识别何时不应信任系统,并为审计人员提供针对性评估的图谱。
-
维护动态模型卡:保留一份简短的、带版本号的文档,记录预期用途、训练数据年份范围、已知盲点(如罕见语言或极端光照)、公平性考量、校准质量,以及操作限制(如批大小、输入形状和延迟预算)。在服务 README 或控制台中链接该文档。
-
定义数据契约与假设:明确预期的单位、范围、编码和模式版本。许多生产故障源于单位不匹配和模式漂移,而非模型本身。同时记录类似“图像被中心裁剪为 224×224”或“人脸至少占画面 20%”的假设。
-
保留边界测试和红队产物:维护一小套对抗性和边缘案例输入,例如模糊、遮挡或反事实编辑,并在每次发布时重新运行。在模型卡中总结任何性能差异。
-
将失败映射到系统响应:对于每个已知的失败模式,明确预期响应:弃权、降级到后备规则或模型,或带警告继续。将这些行为与可测量的触发条件(如漂移指标、OOD 分数或不确定性阈值)挂钩。
例如,对于糖尿病视网膜病变预测,记录性能在超宽视野图像和低于特定分辨率的设备上会下降。部署策略可能要求在分辨率低于该阈值或 OOD 分数超过 0.3 时弃权,并转交眼科医生审查。
人工覆写与审计追踪
本小节探讨可解释性的治理层面:自动决策何时应升级给人工审核者,以及应如何维护审计记录以便日后验证这些决策。可解释性必须接入治理。这里有两个核心的实际要求:清晰的人工覆写路径,以及能够重建决策的审计追踪。
-
定义升级与覆写:写明系统必须转交人工的情况:高不确定性、OOD 检测、策略关键字、不利安全信号。在你的 C++ 服务中,实现“弃权”状态,使其贯穿 API 响应,并集成一个人工审核队列。跟踪人工决策结果以改进再训练数据。
-
不可变的审计日志:对于每个决策,记录:时间戳、请求 ID、模型/推理镜像摘要、预处理和特征版本、输入哈希(或在允许情况下的加密 blob)、预测 + 校准分数、不确定性度量、解释产物(例如 top-k SHAP 条目或 Grad-CAM 热图哈希),以及最终动作(自动 vs 人工)。使日志防篡改(仅追加存储、签名),并通过角色/保留策略控制访问。
-
原因代码与用户申诉:在受监管领域(信贷、就业),为不利行动提供标准化原因代码。提供有文档记录的决策质疑流程,并确保系统能够检索决策时使用的确切产物。
-
关注点分离:在存储和传输中,将个人身份信息(PII) 与模型特征分开;解释应引用特征语义,而非原始 PII。对审计数据实施最小权限访问。
例如,在贷款拒绝决策中,服务返回:decision=deny, prob=0.71 (calibrated)、uncertainty=moderate、原因代码 [INSUFFICIENT_HISTORY, HIGH_UTILIZATION]、ref=REQ-2025-10-05-1234。在内部,审计存储将该引用链接到模型版本 v3.14、特征模式 s2.7、SHAP top-k,以及审核者后续的任何覆写。
付诸实践
在你的代码库中,将伦理脚手架视为一等公民:
-
API:添加诸如
calibrated_score、uncertainty、abstain、reasons[]和audit_ref的字段。 -
服务:实现 OOD 和不确定性门控,将其连接到人工审核队列,并持久化带签名的审计包。
-
文档:随部署说明发布模型卡和数据契约;将其保留在源代码控制中,并在每次发布时更新。
-
运维:将校准、弃权率和审核者一致性作为服务器级指标进行监控,并对异常峰值发出警报。
只有当解释对不确定性坦诚、基于模型实际使用的信息、对已知弱点直言不讳,并嵌入一个允许人类干预和审计人员验证的流程中时,解释才能建立信任。如此使用,可解释性不是自动化之上的一层装饰。它是运营纪律的一部分,使部署的模型与用户、监管者和现实保持一致。
总结
本章将可解释性呈现为一项工程要求,而非可选项。你了解了为什么不同的利益相关者——临床医生、审计人员、风险团队和最终用户——需要不同形式的解释,以及可解释性如何直接与信任、安全和监管相联系。本章还区分了局部解释(阐明单个预测)和全局理解(揭示模型依赖的更广泛模式)。在此过程中,它强调了不确定性(包括偶然不确定性和认知不确定性)应与任何解释一起传达。
在技术方面,你考察了 LIME 和 SHAP 分别估计什么、它们的假设有何不同,以及如何用 C++ 高效实现它们。这包括扰动设计、邻域加权、稀疏代理模型拟合,以及带性能护栏的实用 KernelSHAP 近似。对于卷积神经网络,你还看到了如何用 C++、LibTorch 和 OpenCV 端到端构建 Grad-CAM,方法是捕获激活和梯度、计算通道权重、生成热图,并将其叠加供检查。
本章最后从机制转向负责任的部署。解释不仅必须有用,还必须对不确定性诚实、避免误导,并得到文档、弃权策略和审计追踪的支持。贯穿全文的主要信息是操作性的:解释应足够可靠以指导行动,足够高效以在需要的地方运行,并且足够透明以支持现实世界中的问责。
以下问题有助于巩固本章的主要思想,并让你有机会检查概念理解和实践记忆。
问题
使用以下问题来复习本章的主要概念、方法和实现细节。
-
范围:局部和全局可解释性之间有什么区别?各举一个例子。
-
LIME 与 SHAP(直觉):LIME 实际拟合的是什么?在什么假设下它才能产生有意义的归因?SHAP 估计的是什么?Shapley 值保证了什么关键性质?
-
KernelSHAP(实用性):对于许多特征,为什么精确的 SHAP 难以处理?KernelSHAP 如何近似它?请说出你在 C++ 推理环境中会应用的两个性能技巧。
第13章:深度学习模型中的可解释性与透明度
-
Grad-CAM(机制): 给定目标类别得分 y^c,写出 Grad-CAM 权重 α_k 和粗略热图 M 的公式。为什么要应用 ReLU?
-
C++/LibTorch: 在 LibTorch 模型中,如何捕获最后一个卷积层的激活及其梯度以计算 Grad-CAM?请给出相关的 API 调用。
-
稳定性检查: 说出两种有助于检测显著性/Grad-CAM 类方法中误导性归因的合理性检查。
-
伦理: 举出两个例子,说明在受监管的产品中,即使解释在技术上正确,也可能具有误导性。
-
弃权策略: 系统何时应返回“我不知道”并转交人工审核?请说出至少两种可用于触发弃权的信号。
-
模型卡: 列出模型卡中应包含的四个项目,用于记录已知故障模式和运行限制。
-
隐私与对抗利用: 为什么应避免公开暴露原始 SHAP 值?为最终用户提供一种更安全的替代方案。
-
校准: 什么是期望校准误差(ECE)?为什么在解释旁边显示概率时它很重要?
进一步阅读
以下参考文献为本章涵盖的主要可解释性方法提供了更深入的内容,并提供了关于归因、显著性、文档编制和负责任模型治理的更广泛视角。
- Ribeiro、Singh、Guestrin(2016)。我为什么要相信你? 解释任何分类器的预测——LIME
- Lundberg 和 Lee(2017)。解释模型预测的统一方法 — SHAP 理论
- Selvaraju 等人(2017)。Grad-CAM:基于梯度定位的深度网络视觉解释
- Sundararajan 等人(2017)。深度网络的公理化归因 — 积分梯度
- Smilkov 等人(2017)。SmoothGrad:通过添加噪声来去除噪声
- Adebayo 等人(2018)。显著性图的合理性检查
- Mitchell 等人(2019)。用于模型报告的模型卡
- Gebru 等人(2018)。数据集数据表
- Doshi-Velez 和 Kim(2017)。走向可解释机器学习的严谨科学
- Molnar,《可解释机器学习》(书籍,在线持续更新)
- 监管背景:欧盟《人工智能法案》(风险等级、透明度义务);GDPR(通用数据保护条例)中“解释权”的讨论;行业指南(例如 FDA 的 SaMD(软件即医疗器械)、信用模型原因代码)
答案
以下答案对复习问题提供了简明扼要的解释,并强调了本章涉及的关键概念、方法和实现要点。
-
局部与全局:局部解释单个预测(例如,单一贷款申请的 SHAP 值)。全局概括整体模型行为(例如,某个队列中的特征重要性趋势、部分依赖图)。在个案决策中使用局部解释,在审计和政策制定中使用全局解释。
-
LIME 与 SHAP:LIME 在实例周围拟合一个稀疏、加权的线性代理模型,假设在选定的邻域或扰动分布下具有局部线性。SHAP 估计Shapley 值(每个特征在联盟中平均的边际贡献),这些值具有可加性,并满足有效性、对称性和零玩家等性质。
-
KernelSHAP 及技巧:精确 SHAP 随特征数量呈指数增长;KernelSHAP 使用带 SHAP 核的加权线性回归在采样的联盟上进行。性能提示:(i)通过筛选或特征分组来限制特征集;(ii)缓存模型评估结果;(iii)使用低差异采样;(iv)在少量代表性基线背景上运行;(v)在 C++ 中使用时间或迭代上限来限制运行时间。
-
Grad-CAM 公式:Grad-CAM 通道权重计算为类别得分对每个特征图的梯度的空间平均值:
α_k^c = (1/(H × W)) Σ_{i,j} (∂y^c / ∂A^k_{i,j})。
然后粗略热图表示为 M = ReLU(Σ_k α_k^c A^k)。
ReLU 仅保留对目标类别得分有正贡献的区域,这使热图更容易被解释为该类别的支持证据。
-
LibTorch 捕获:在
forward()中保存目标卷积激活,并对其调用retain_grad()。在从标量类别得分反向传播后,读取last_conv_activation.grad()。或者,注册一个前向钩子来捕获激活。关键 API:retain_grad()、backward()、eval()以及张量.grad()访问。 -
合理性检查:
- 随机化权重:归因应崩溃
- 标签打乱:显著性应退化
- 输入对称性:小的旋转/裁剪不应完全翻转解释
- 插入/删除测试:扰动高归因区域;预测应相应做出反应
-
误导性解释:
- 在敏感/受保护属性上展示细粒度的 SHAP 值可能隐含因果关系,或引发歧视主张
- 对误定位输入使用 Grad-CAM 可能会突出背景,当模型依赖的是虚假上下文时,这会带来虚假的安慰(“模型看了肿瘤区域”)
-
弃权触发条件:预测熵高或置信度间隔接近零;OOD(分布外)检测器超过阈值;校准漂移超出服务等级目标(SLO);关键特征缺失或无效;与规则或启发式方法不一致。将这些情况转入人工审核队列。
-
模型卡项目:预期用途/限制;训练数据的时间范围与覆盖度;已知故障模式(例如,稀有语言、极端光照);校准质量;公平性说明;运行约束(输入形状、延迟预算);按队列划分的评估指标;更新/回滚策略。
-
隐私与对抗利用:原始 SHAP 值可能泄露敏感属性以及用户可能利用的阈值。最好采用原因代码或粗略分组因子(例如“还款历史”、“使用率”)并附上说明,而不是原始数字。对对外可见的解释进行速率限制和聚合。
-
期望校准误差(ECE):它衡量预测概率与观测频率在不同分数区间之间的差距。低 ECE 意味着“预测概率为0.8的事件实际也会以80%的频率发生”。这很重要,因为用户(以及下游阈值和工作流)依赖概率的可信度。
获取本书的 PDF 版本及更多内容
扫描二维码(或访问 packtpub.com/unlock)。按名称搜索本书,确认版本,然后按照页面上的步骤操作。
https://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/deep-learning-with-c/img/f45df257dfb11cd9b58ac2c5f6935210_583_0.pnghttps://github.com/OpenDocCN/freelearn-dl-zh/tree/master/docs/deep-learning-with-c/img/f45df257dfb11cd9b58ac2c5f6935210_583_1.png
注意:请随身携带您的发票。直接从 Packt 购买则无需发票。
14 解锁您的专属权益
您持有的这本书包含以下专属权益:
请按照以下指南解锁。该过程只需几分钟,且只需完成一次。
通过 3 个简单步骤解锁本书的免费权益
第1步
请准备好购买发票,以备第3步使用。如果您持有实体书,请用手机扫描并将其保存为 PDF、JPG 或 PNG 格式。
如需有关查找发票的更多帮助,请访问 https://www.packtpub.com/en-us/unlock?step=1。
注意:如果您直接从 Packt 购买本书,则无需发票。完成第2步后,您就可以立即访问专属内容。
第2步
扫描二维码或访问 packtpub.com/unlock。
在打开的页面上(类似于桌面上的图14.1),按名称搜索本书并选择正确的版本。
图14.1:Packt 桌面端解锁登录页
第3步
选择书籍后,登录您的 Packt 账户或免费创建一个。然后上传您的发票(PDF、PNG 或 JPG,最大 10 MB)。按照屏幕上的说明完成操作。
需要帮助
如果您遇到困难需要帮助,请访问 https://www.packtpub.com/unlock-benefits/help,查看关于如何查找发票等问题的详细常见问题解答。此二维码将带您进入帮助页面。
注意:如果您仍然遇到问题,请联系 customercare@packt.com。
packtpub.com
订阅我们的在线数字图书馆,即可完整访问超过 7,000 本图书和视频,以及行业领先的工具,帮助你规划个人发展、推进职业发展。如需了解更多信息,请访问我们的网站。
为什么订阅?
- 通过 4,000 多位行业专业人士的实用电子书和视频,减少学习时间,增加编码时间
- 使用专为你定制的技能计划提升学习效果
- 每月免费获取一本电子书或一个视频
- 支持全文搜索,轻松访问关键信息
- 可复制、粘贴、打印和收藏内容
在 packtpub.com 上,你还可以阅读一系列免费技术文章、订阅多种免费通讯,并获得 Packt 图书和电子书的专属折扣和优惠。
你可能还喜欢的其他图书
C++ 机器学习实战
Kirill Kolodiazhnyi
ISBN: 978-1-80512-057-5
- 利用各种 C++ 库应用关键机器学习算法
- 将不同数据类型加载并预处理为适合的 C++ 数据结构
- 了解如何为机器学习模型确定最佳参数
- 使用异常检测过滤用户数据
- 应用协同过滤管理动态用户偏好
- 利用 C++ 库和 API 管理模型结构与参数
- 使用现代神经网络实现目标检测的 C++ 代码
深度学习数学练习册
Tom Yeh
ISBN: 978-1-80667-477-0
- 将手工计算与现代深度神经网络的行为联系起来
- 将深度学习数学拆分为可解决的小谜题
- 揭示点积、矩阵乘法和归一化等核心运算背后的含义
- 了解线性层、激活函数和损失函数如何组合在一起
- 将简单神经元的数学与现代深度网络联系起来
- 通过逐步推理而非死记公式,培养对 AI 的真正直觉
Packt 正在寻找像你一样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packt.com 并立即申请。我们已经与数千名像你一样的开发者和技术专业人士合作,帮助他们向全球技术社区分享见解。你可以进行一般性申请,申请我们正在招募作者的特定热门主题,或提交自己的想法。
分享你的想法
既然你已经读完了C++ 深度学习,我们非常希望听到你的想法!如果你从亚马逊购买了这本书,请点击此处直接前往本书的亚马逊评论页面分享你的反馈,或在购买本书的网站上发表评论。
你的评论对我们和技术社区都非常重要,将帮助我们确保提供优质的内容。
索引
A
- 操作
- 警报发生时触发 506-508
- 激活函数 144
- C++ 中的激活函数
- 使用 LibTorch 实现 116-120
- AdaDelta 162-165
- 概念与技术 165
- AdaGrad 160-162
- 自适应矩估计(Adam)127
- 优化器 158-160
- 对抗正则化自编码器(ARAE) 287
- 亚马逊云服务(AWS) 79
- 存档谱系 450
- Armadillo
- 用于特征交互工程 56
- 用于特征缩放与标准化 53
- 关联规则学习 11
- 非对称量化 359
- 异步流 492
- 注意力机制
- 变革 311, 312
- 带线性偏置的注意力(ALiBi) 327
- 数据增强技术 70-72
- 增强(词元丢弃)71
- 图像增强 70
- 数值数据增强 71
- 文本增强 70
- 自编码器(AEs) 33, 267
- 架构 268
- 使用 LibTorch 的 C++ 实现 268, 269
- 损失函数 268
- 重构误差 268
- 自回归生成与因果语言建模 288, 289
- N-gram 300
- 采样策略 292
- 温度与采样策略 290
- 温度缩放 291
- TF-IDF 300
- 可用性 458
- 平均预测概率 462
B
- 随时间反向传播(BPTT) 216
- C++ 实现 218
- 数学基础 216
- 向后填充 35
- 反向传播 121
- 基本线性代数子程序(BLAS) 179
- 批量(分块)处理 73
- 批量梯度下降 123
- 批大小分布 462
- 束搜索 293-295
- BERTScore 305, 306
- 来自 Transformer 的双向编码器表示(BERT) 57, 337
- 特点 338
- 双语评估替补(BLEU) 302, 303
- 基于 Transformer 表示的双语评估替补(BLEURT) 305
- 二元交叉熵 111-113
- 二进制编码 39
- 蓝/绿(或 A/B) 409
- 有界队列 390
- 构建工具 18
- CMake 18
- Make 18
- 字节对编码(BPE) 57, 245, 246
C
- C++
- 实现 173
- 实现 SHAP 527
- 基于 C++ 的神经网络
- 构建图像分类模型 183-186
- C++ 代码
- 使用 CUDA 优化 86
- C++(用于深度学习)
- 优点 12, 13
- 在本地搭建开发环境 17-22
- 使用 Google Colab 13-17
- 关键性能优势 22-24
- 环境搭建 13
- 实现简单神经网络 24, 25
- C++ 前端
- 紧凑模型 376, 377
- BPTT 的 C++ 实现 218
- 基于 Eigen 的实现 220
- 基于 LibTorch 的实现 220
- 基于向量的实现 219, 220
- 生成对抗网络(GANs)的 C++ 实现 283
- 判别器网络架构 285
- 生成器网络架构 284
- 训练 285-287
- 神经网络量化的 C++ 实现 362
- 用于 Transformer 的逐通道量化 362, 363
- 在 C++ 中实现 PTQ 363, 364
- 在 C++ 中实现 QAT 365, 366
- 使用 LibTorch 的 C++ 实现
- 瓶颈层 269
- 解码器 269
- 编码器 268
- 前向传播实现 270
- 训练循环 270, 271
- C++ 插桩
- 设计原则 481, 482
- 指标原语(metrics.hpp)482
- Prometheus 风格指标 479
- 结构化日志 479
- 追踪与跨度 479
- 校准 468, 505
- 插桩 470, 471
- 读取数值 472
- 金丝雀发布 449
- 金丝雀切换 409
- 因果语言建模 288
- 字符级分词 244
- 卡方(X²)检验 416, 420
- 类别分布 418
- 缺陷类别
- 并发与同步缺陷 439, 440
- 数据契约缺陷 438
- 隔离 437
- 数值缺陷 437, 438
- 点击率(CTR) 417
- CLion 18
- 云 390, 406
- 基于云的 CUDA 环境
- 使用 82-86
- 聚类 11
- CMake 18
- 合并访问 389
- Code::Blocks 18
- 队列 464, 492
- 队列划分 498-501
- 常见激活函数 9
- ReLU 8
- sigmoid 8
- tanh 8
- 统一计算设备架构(CUDA)
- 检查 API 调用 100
- 在 C++ 中实现数组加法 87, 88
- 检查异步执行错误 101
- 代码转换 89
- 调试 100
- 错误处理函数 102, 103
- 完整实现 90, 91
- 安装 79
- 检查内核启动错误 101
- 启动内核 89, 90
- 使用多个块扩展 96-100
- 使用多个线程 93-96
- 性能剖析 92
- 编程模型 86, 87
- 环境搭建 79
- 线程层次结构 92, 93
- 概念漂移 416, 418
- 容器化 383
- 连续词袋模型(CBOW) 249
- 契约违约 502
- 受控并行性 390
- 卷积
- 数学基础 171, 172
- 卷积神经网络(CNNs) 169, 376
- 特征图 205
- 使用 Grad-CAM 536, 537
- 历史 170
- 池化 204
- 步长 205
- 术语 203, 205
- 计数器 460
- 纯 CPU 应用
- 与 GPU 加速应用对比 78, 79
- CPU 利用率 476
- 用于触发操作的数值 478
- 裁剪 193, 194
- 交叉注意力 316, 332
- 交叉熵损失 303
- 跨语言翻译评估优化指标(COMET) 305
- cuBLAS 操作 179
- CUDA 382
- 用于 GPU 加速实现 178-181
- 用于实现 MLP 135-139
- CUDA Graph
- 为固定形状推理进行捕获 397, 398
- CUDA 实现
- 以高昂成本实现最大性能 141
- CUDA 随机数生成器(cuRAND) 179
索引
- CUDA 线程
- 与 CPU 线程对比 87
- CUDA 工具包
- 安装依赖 82
- 下载 80, 81
- 安装 80, 81
D
- 暗知识 352
- 数据增强 70
- 数据契约缺陷 438, 439
- 数据契约违反 426
- 数据漂移(协变量偏移)409
- 数据管道
- 用于集成 PyTorch Dataset API 72
- 数据预处理
- 在深度学习中(DL)31
- 作用 32
- 数据集 110
- DDP 实现
- 分布式采样器 340
- 基于 Epoch 的洗牌 340
- DDPTrainer 类 341
- 分布式采样 343, 344
- 梯度 All-Reduce 342
- 参数广播 341
- 训练循环 343, 344
- 决策边界 111
- 解码器 268
- 解码器路径(扩展路径)188
- 解码器堆栈实现 330
- 前馈网络 333
- 层堆叠 333, 334
- 多头交叉注意力 332, 333
- 处理流程 331, 332
- 反卷积 191
- 深度学习(DL)7, 77
- C++ 的优势 12, 13
- 数据预处理的重要性 31
- 可解释性的重要性 516, 517
- 解释 517
- 图像处理 191
- 神经网络 7, 8
- 强化学习(RL)9, 12
- 半监督学习 9, 12
- 监督学习 9, 10
- 文本处理流水线 254
- 无监督学习 9, 11
- 深度学习训练概念
- 激活函数 144
- AdaDelta 162-165
- AdaGrad 160-162
- ADAM 优化器 158-160
- 高级函数 150-152
- 探索 144
- 梯度下降 153, 155
- 动量 156-158
- 优化技术 152, 153
- ReLU 系列 146-150
- RMS Prop 155, 156
- sigmoid 系列 145, 146
- 延迟标签 492-495
- 处理 427, 428
- 去噪自编码器 271
- 已部署模型
- 特性 411
- 结构化数据详细示例
- 降维 44, 45
- 特征交互工程 49, 51
- 特征缩放与标准化 41-44
- 缺失值处理 33-36
- 编码类别特征的技术 37-40
- 时间序列工程 46-48
- 设备到主机(D2H)464
- 降维 11
- 判别模型 266
- 蒸馏 395, 396
- C++:一个微型基准测试框架 396, 397
- 分布式数据并行(DDP)310, 339
- DDPTrainer 类 341
- 实现 340
- 分布式采样器 343
- 分布漂移 415, 416
- 漂移
- 类别特征 419
- 比较与测试 420
- 处理 416
- 健康信号 419
- 识别 416
- 实现 417, 418
- Kullback-Leibler(KL)420
- 数值特征 419
- 预测 419
- 分类 419
- 漂移预算 505
- 漂移监控
- 落地实施 428, 429
- 动态批处理 390
- 动态并行 89
E
- Eclipse CDT 18
- 边缘 390, 406
- Eigen 176
- 用于基于矩阵的实现 176
- 基于 Eigen 的方法 214, 216
- 基于 Eigen 的实现 220
- Eigen 实现 141
- 特征值分解(EVD)51
- 嵌入编码 40
- 编码器 268
- 编码器路径(收缩路径)188
- 编码器堆栈实现 329
- 加法运算 330
- 前馈网络 330
- 层堆叠 330
- 归一化运算 330
- 处理流程 329
- 自注意力机制 329
- 端到端形状 406, 407
- 错误预算 505
- 消耗 458
- 错误率、吞吐量 458
- 伦理考量 543
- 审计跟踪 545, 546
- 伦理框架 546
- 人工接管 545, 546
- 记录已知故障模式 545
- 避免误导性归因 544
- 沟通不确定性时不作虚假精确 543, 544
- 生成模型的评估指标 301
- 分类 302
- 基于模型的指标 305
- 基于事件的重新训练 410
- 以证据为先的运行手册
- 使用 444, 445
- 证据下界(ELBO)274
- 执行配置 78
- 期望校准误差(ECE)416, 458, 468, 469, 543
- 深度学习的可解释性
- 与信任和法规的关联 518
- 与安全性的关联 518
- 应用实用设计原则 519
- 利益相关者需求 518
- 指数移动平均(EMA)46
F
- 快速失败 406
- 故障
- 使可复现 435-437
- 快速傅里叶变换(FFT)48
- 特征(协变量)漂移 417
- 特征图 205
- 特征 110
- 前馈网络 333, 334
- 翻转 197, 198
- 前向填充插补 34
- 前向传播 121
- 前向传播与损失
- 构建 111-115
- 前向传播实现 174, 175
- FP16 混合精度
- C++ TorchScript 推理 392, 393
- 频率编码 38
- 全分片数据并行(FSDP)310, 344, 345
- All Gather 345, 346
- FSDPTrainer 类 347-351
- Reduce-scatter 346, 347
G
- GAN 架构
- 判别器网络 280
- 生成器网络 280
- 训练动态 281
- GAN 损失函数 281
- 判别器损失 281
- 生成器损失 281
- 生成对抗网络(GANs)279
- GAN 训练过程 282, 283
- 替代损失函数 283
- 门控循环单元(GRUs)311
- 仪表 460
- 高斯误差线性单元(GELU)149
- 生成模型 266, 267
- 评估指标 301
- 生成式预训练 Transformer(GPT)338
- 特性 338
- 与 BERT 对比 339
- 与 GPT 对比 339
- 词表示的全局向量(GloVe)57
- 谷歌云平台(GCP)79
- Google Colab 13
- GPU 加速应用
- 与仅 CPU 应用对比 77-79
- GPU 内存余量 458
- GPU 利用率 476
- 数字解读 477
- Grad-CAM 图
- 从激活和梯度计算 539, 540
- 运行端到端演示 540, 542
- 梯度提升机(GBMs)36
- 梯度裁剪 217
- 梯度下降 153, 155
- 通过门控的梯度流 236
- 参数梯度累积 236
- 实际实现注意事项 236
- 梯度
- 推导/反向传播 121-124
- 梯度信号保留(GraSP)356
- 梯度加权类激活映射(Grad-CAM)520
- 在真实网络上应用 542, 543
- 在真实约束下组合方法 526
- 重定向到 CNN 聚焦的类别决策 525, 526
- 用于 CNN 536, 537
- 图形处理单元(GPU)77
- 贪心解码 290-293
- gRPC 405
- 护栏
- 用于触发较慢循环 508, 509
H
- 异构并行计算 78
- 合页损失 283
- 主机到设备(H2D)464
- 热修复 425
- HTTP 405
- 人在回路通道 450
- 双曲正切(tanh)145
- 滞后 502
I
- 用于 C++ 设置的 IDE
- CLion(跨平台)18
- Code::Blocks(跨平台)18
- Eclipse CDT(跨平台)18
- Visual Studio(Windows)17
- 图像分类
- 构建基于 C++ 的神经网络 183-186
- ImageNet 大规模视觉识别挑战赛(ILSVRC)171
- 用于深度学习的图像处理 191
- 裁剪 193, 194
- 翻转 197, 198
- 填充 199
- 重采样 200-203
- 旋转 191, 192
- 缩放 194
- 平移 192, 193
- 放大 196
- 图像分割
- 使用 U-Net 186-191
- C++ 实现 173
- 使用 PyTorch C++ API 的卷积层 182, 183
- 前向传播实现 174, 175
- 使用 CUDA 的 GPU 加速实现 178-182
- 与更大网络的集成 175, 176
- 使用 Eigen 的基于矩阵的实现 176-178
- 推理 336, 337
- 进程内度量 433
- INT8
- C++ TorchScript 推理 393
- 在 C++ 中运行量化 ONNX 模型 393, 394
- 集成开发环境(IDE)79
- Unicode 国际组件(ICU)237
- 四分位距(IQR)43
- 逆文档频率(IDF)300, 305
- 迭代剪枝 358
J
- 詹森-香农(JS)散度 420
K
- 内核 78
- KernelSHAP 531
- 关键绩效指标(KPI)426
- K 近邻(k-NN)33
- k-NN 插补 35
- 知识蒸馏 352
- 双重目标 353-355
- 科尔莫戈罗夫-斯米尔诺夫(KS)检验 416
- KS 检验 420
L
- 标签 110, 484
- 大语言模型(LLMs)458
- 大规模数据集
- 批处理 73
- 管理 73
- 内存映射 73
- 延迟 458, 472, 473
- 隐空间 268
- 先行指标 409, 492
- 学习到的位置嵌入 319
- 最小二乘损失(LSGAN)283
- 基于库的预处理 51
- 使用 mlpack 编码类别特征 52
- 降维 54
- 使用 Armadillo 进行特征交互工程 56
- 使用 Armadillo 进行特征缩放与标准化 53
- 使用 mlpack 处理缺失值 51
- 标准模板库(STL)52
- 使用 mlpack 和 OpenCV 进行时间序列工程 55
- LibTorch
- C++ 实现 250-253
- 使用(LibTorch)构建序列到序列翻译器 259
- 用于实现 MLP 139, 140
- 用于在 C++ 中实现神经元和激活函数 116-120
- 用于 C++ 实现 268
- 基于 LibTorch 的实现 220, 231, 232
- 裁剪 233, 234
- 梯度缩放 233
- 混合精度训练 232
- LibTorch 实现
- 生产系统的实用默认配置 142
- 轻量级监控结构
- 实现 422-425
- 线性回归 10, 111
- 局部可解释模型无关解释(LIME)520-522
- 保真度故障排除 536
- 实现 527-531
- 呈现 536
- 逻辑回归 10, 111
- 逻辑 S 型函数(标准 S 型函数)145
- 长短期记忆(LSTM)209, 309
- 损失函数 268
- LSTM 反向传播 234
- 通过门控的梯度流 236
- 数学框架 235
- LSTM 网络 221, 222
- C++ 实现 225
- 信息流动态 224, 225
- 与……集成 253, 254
- 数学公式 223, 224
L
- 内存高速通道 222, 223
- LSTM 网络,C++ 实现 225
- 基于 LibTorch 的实现 231
- 基于矩阵的实现 227-230
- 基于向量的实现 225
M
- 机器学习(ML)109
- Make 18
- 数学公式
- 特征 150, 151
- 用于 BPTT 的数学基础 216
- 时间上的梯度流 216
- 参数梯度累积 217
- 实际实现注意事项 217, 218
- 最大似然估计(MLE)44
- 均方误差(MSE)111, 133, 268
- 梅尔频率倒谱系数(MFCCs)56
- 内存映射 73
- 内存利用率 476
- 插桩检测 476, 477
- 读取数值 477
- 显式排序翻译评估指标(METEOR)302-305
- 指标 407, 429, 430
- 微批处理器 402
- 带并发控制 383-389
- 微批处理 462
- 小批量 SGD 124
- 最小 CNN
- 构建,用于展示目标卷积特征 537, 538
- 最小-最大缩放 41
- mlpack
- 用于编码分类特征 52
- 用于处理缺失值 51
- MLP 实现方法
- CUDA 实现 141
- 混合方法 142, 143
- LibTorch 实现 142
- 原始 C++(使用 Eigen)141
- C++ 中的 MLP
- Layer 类 131, 132
- MultilayerPerceptron 类 132-135
- 模型压缩技术 351
- 知识蒸馏 352
- 剪枝 355-357
- 量化 359, 360
- 模型监控
- 实现 464-467
- 重要性 458, 460
- 模型并行 344
- 模型并行化 339
- DDP 339
- 修改版美国国家标准与技术研究院(MNIST)170
- 动量 156, 158
- 监控层 458
- 多头注意力机制 316, 317
- C++ 实现 317, 318
- 多头交叉注意力 332
- MultilayerPerceptron 类 132-135
- 多层感知机(MLP)109, 127, 169
- 架构 129
- 选择实现方法 141
- 在 C++ 中实现 130
- 使用 CUDA 实现 135-139
- 使用 LibTorch 实现 139, 140
N
- 命名实体识别(NER)57, 212
- 原生 LibTorch 前向传播 379
- 自然语言处理(NLP)56
- 神经网络(NN)7, 8, 33
- C++ 中的神经元
- 使用 LibTorch 实现 116-120
- N-元语法 300, 301
- 非线性边界 111
- 非平稳数据生成过程 417
- 归一化策略 69, 70
- 图像数据 69
- 数值数据 69
- 文本数据 69
- 文本归一化(嵌入)70
- 核采样 296
- 数值错误 437
- NVIDIA 集合通信库(NCCL)341
- NVIDIA 驱动程序
- 安装 79, 80
- NVIDIA 管理库(NVML)476
O
- 可观测性 407
- 独热编码 37
- 在线准确率 468
- 插桩检测 470
- 读取数值 472
- 在线质量
- 接入遥测 500-502
- 监控 492
- ONNX 375
- ONNX Runtime 375, 381
- 配置选项 399
- 用于运行时调优 398
- 本地部署 390, 406
- OpenCV 辅助工具
- 用于图像到张量转换和叠加 538, 539
- 开放神经网络交换(ONNX)51
- 从 C++ 中使用以实现可移植性 381, 382
- 序数编码 39
- 分布外(OOD)543
- 内存不足(OOM)350, 429
P
- 填充 199
- 一致性 379
- 使用原生图检查 379, 381
- ONNX,从 C++ 中使用以实现可移植性 381, 382
- 保持运行时假设一致 382
- 个人身份信息(PII)546
- 策略
- 测试 509, 510
- 池化
- 平均池化 204
- 最大池化 204
- 群体稳定性指数(PSI)416
- 可移植操作系统接口(POSIX)73
- 位置编码 319, 331
- 其他现代方法 327
- 实现注意事项 327
- 学习的位置嵌入 319
- RoPE 324
- 正弦位置嵌入 320-323
- 可预测推理 400
- 预测漂移 417
- 预训练语言模型 337
- BERT 337
- GPT 338
- GPT 与 BERT 对比 339
- 主成分分析(PCA)33, 44
- 先验(标签)偏移 418
- 生产环境调试 429
- 生产环境推理 382
- 剪枝 355-357, 394
- C++ 加载剪枝后的 TorchScript 395
- 流水线概述 357, 358
- PyTorch C++ API
- 与 ConvolutionLayer 一起使用 182, 183
- PyTorch C++ API(LibTorch)19
- PyTorch 数据集 API
- 集成,用于数据流水线 72
Q
- 质量信号 495-497
- 量化 358-360, 392, 478
- 用于神经网络的 C++ 实现 362
- 蒸馏 395, 396
- FP16 混合精度(TorchScript、CUDA)392
- FP16 混合精度(ONNX Runtime、Q/DQ 图)393
- 训练后量化(PTQ)361
- 剪枝 394
- 量化感知训练(QAT)361
- 范围选择策略 360
- 量化感知训练(QAT)361
- 队列时间
- 与计算时间分离 440-444
- 队列等待时间 462
R
- 随机 ReLU(RReLU)149
- 实时推理服务 400
- API 边缘 400
- 推理核心 400
- 微批调度器 401
- 调度器 400
- 实时服务器 391
- 实时服务 400
- 重新校准方法 462
- 面向召回率的摘要评估指标(ROUGE)302, 304
- 重构误差 268
- 循环神经网络(RNNs)309
- 架构 211
- C++ 实现 214
- C++ 实现,基于 Eigen 的方法 214
- 数学基础 212, 213
- 记忆动态与信息流 214
- 类型 212
- 红、绿、蓝(RGB)184
- 感兴趣区域(ROI)193
- 注册表 484, 486
- 强化学习(RL)9, 12
- 相对位置偏置(T5)327
- ReLU 家族 146-150
- 重采样 200-203
- 常驻内存大小(RSS)476
- 资源获取即初始化(RAII)73, 233
- 有目的的重训练 445
- 具体场景 450
- 上线后的闭环 450
- 实现确定性 446-448
- 在服务约束下的模型评估 448
- 场景 445, 446
- 影子评估 449, 450
- 稳健优化的 BERT 预训练方法(RoBERTa)57
- 鲁棒缩放 43
- 均方根传播(RMSprop)155, 156
- 旋转 191, 192
- 旋转位置嵌入(RoPE)309, 324
- 点积对相对位置的依赖 325-327
- 偶数和奇数维度的配对 324
- 路由 389
- 运行时调优 397
- LibTorch 中的选项 397
S
- 采样策略 292
- 束搜索 293, 294
- 贪心解码 292
- Top-K 采样 295, 297, 298
- 缩放指数线性单元(SELU)148
- 缩放 194
- 调度层 402
- 紧凑型微批处理器 402, 404
- 安全外壳(SSH)83
- 自注意力机制 312
- C++ 实现 314, 315
- 数学基础 313, 314
- 半监督学习 9, 12
- 基于策略梯度的序列生成对抗网络(SeqGAN)287
- 序列到序列(Seq2Seq)自编码器 278
- 序列到序列转换器
- 使用 LibTorch 构建 259
- 编码器-解码器前向传播 261
- 通过自回归解码进行推理 262
- 使用教师强制训练 261
- 词汇表构建与词元管理 260
- 序列数据 210
- 服务契约
- 使其明确且可测试 401
- 服务级别指标(SLIs)458-461
- 迁移到策略 502, 503, 504
- 服务级别目标(SLOs)373, 416, 458, 460
- 说明 535
- 影子部署 409
- 影子标签 409
- 影子模式 449
- SHapley 加法解释(SHAP)522, 531-535
- 在 C++ 中实现 527
- 分片 344
- sigmoid 111, 118
- Sigmoid 家族 145, 146
- SIMD 指令 23
- 简单神经网络
- 在 C++ 中实现 24, 25
- 单指令多数据(SIMD)30, 176
- 单次网络剪枝(SNIP)356
- 奇异值分解(SVD)51
- 正弦位置嵌入 320-323
- Skip-gram 模型 249
- 数学公式 249
- SLA 460
- SLO 契约 409
- Softplus 145
- Softsign 146
- 跨度 461, 489, 491
- SparseGPT 356
- 堆叠层 111
- 标准模板库(STL)52, 134
- 统计指标 302
- BLEU 303
- 交叉熵损失 303
- METEOR 304
- 困惑度 302
- ROUGE 304
- 随机梯度下降
- 用于更新权重 121-124
- 随机梯度下降(SGD)119, 140
- 停用词
- 在序列文本处理中 246, 247
- 流式多处理器(SMs)96
- 步长 205
- 结构化数据
- 详细示例 33
- 基于库的预处理 51
- 预处理技术 33
- 与非结构化数据的对比 32
- 结构化日志 429-432, 488, 489
- 结构化剪枝 355
- submit() 方法 402
- 子词分词 245
- 监督学习 9, 10
- 线性回归 10
- 逻辑回归 10
- 持续偏差 420
- 对称量化 360
T
- 尾部 458
- 尾部延迟准则 405
- 目标 110
- t 分布随机邻域嵌入(t-SNE)33
- TensorFlow C++ API 22
- 张量并行 345
- TensorRT 382
- 词频-逆文档频率(TF-IDF)300
- 词频(TF)300
- 测试 110
- 文本处理 237
- C++ 中的增强字符串操作 237, 238
- 用于文本数据的文件 I/O 操作 238-240
- 字符串操作与处理 240-248
- 文本处理流水线 254, 255
- RNN 在语言任务上的应用 255-259
- 使用 LibTorch 构建序列到序列转换器 259
- 吞吐量 473, 474
- 插桩检测 474, 475
- 读取数值 475
- 基于时间的重新训练 410
- 时间序列 210
- 首字节/首词元时间(TTFB/TTFT)458
- 分词 243
- Top-K 采样 295-298
- 缺点 298
- 终止条件 298, 299
索引
- TorchScript 375
- 加载 378, 379
- 运行 378
- TorchScript 产物
- 生成 378
- TorchScript CPU 390
- TorchScript 前向 379, 381
- 追踪 408, 429-435, 489
- 训练 336
- Transformer 架构 310, 311
- 注意力机制的革命 311, 312
- 多头注意力,C++ 实现 317, 319
- 多头注意力机制 316, 317
- 自注意力,C++ 实现 314-316
- 自注意力,数学基础 313, 314
- 自注意力机制 312
- Transformer 实现 328, 329
- 架构 335, 336
- 解码器堆栈实现 330
- 编码器堆栈实现 329
- 训练与推理对比 336, 337
- 翻译 192, 193
- 截断的 BPTT 217
- t-SNE 45
U
- U-Net 188
- 用于图像分割 186-191
- 统一内存 78
- 均匀流形近似与投影(UMAP) 45
- 非结构化数据 57
- 音频/视频数据 68, 69
- 图像数据 67
- 文本数据 57, 58
- 与结构化数据对比 32
- 非结构化数据,文本数据
- 基于嵌入的向量化 60, 61
- 命名实体识别/情感分析/词形还原(ONNX Runtime) 64-67
- 序列填充与截断(仅头文件辅助工具) 63
- 词干提取 58
- 停用词去除 58
- 分词 58
- 非结构化剪枝 355
- 无监督学习 9-11
V
- VAEs 271
- C++ 实现 274-279
- 重参数化技巧 272-274
- VAEs 架构
- 解码器网络 271
- 编码器网络 271
- 采样层 271
- 基于向量的实现 219, 220, 225
- 细胞状态与隐藏状态更新 227
- 核心 LSTM 架构 225
- 门控计算实现 226
- 初始化策略 226
- 数值稳定性考虑 226
- Visual Studio 17
W
- Wanda 356
- Wasserstein 损失(WGAN) 283
- 加权线性回归 531
- Word2Vec 249
- 词嵌入 248
- 词错误率(WER) 426
- 词级分词 244
Z
- 缩放 196
- Z 分数归一化 42

浙公网安备 33010602011771号