案例 3:从逻辑神经元到 NumPy 深层二分类网络#

学习目标与记号#

  本案例只用 NumPy 手工实现一个多隐藏层全连接神经网络,预测 UCI 数据中的 M/B 标签。重点不是追求最高准确率,而是把正文中的神经元、批量前向传播、缓存、链式法则和误差信号逐一落实为可以检查的数组运算。

  本案例用 \(n\) 表示样本量,用 \(d_l\) 表示第 \(l\) 层的神经元数量;批量样本按行存放,输入矩阵记为 \(\boldsymbol{X}\in\mathbb{R}^{n\times d_0}\)层编号写作上标 \([l]\)主要学习目标如下:

  1. 说明权重、偏置、线性运算结果和激活值的维度;

  2. 写出多层前向传播与二元交叉熵,并说明偏置为何能沿样本轴广播;

  3. 根据前向传播保存的中间结果手工实现后向传播,并用数值梯度检查关键参数;

  4. 将逻辑回归作为简单基线,在验证集上选择阈值,确定模型与训练方案后只评价一次测试集;

  5. 区分算法教学演示与真实医疗用途。

  对应正文: “从逻辑回归到神经元”“单隐藏层神经网络——单样本情形”“单隐藏层神经网络——多样本向量化”“多隐藏层全连接神经网络”和“用 NumPy 实现全连接神经网络”。

数据来源、下载方式与边界#

  • 数据:UCI Breast Cancer Wisconsin (Diagnostic)

  • 说明页:https://archive.ics.uci.edu/dataset/17/breast-cancer-wisconsin-diagnostic

  • 匿名 HTTPS 直链:https://archive.ics.uci.edu/static/public/17/breast+cancer+wisconsin+diagnostic.zip

  • 许可:CC BY 4.0

  • 规模:569 行,30 个连续特征,标签为 M 或 B。

  这些特征由细针穿刺图像计算得到。本案例仅用于解释神经网络计算,既不建立临床因果关系,也不能用于诊断、分诊或治疗。下载文件写入 AI_COURSE_DATA_DIR;若未设置,则写入用户缓存目录,不污染课程源码树。

from pathlib import Path
import hashlib
import io
import os
import shutil
import tempfile
import urllib.request
import zipfile

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

SEED = 42
rng = np.random.default_rng(SEED)
CACHE_ROOT = Path(os.environ.get('AI_COURSE_DATA_DIR', Path.home() / '.cache' / 'ai-course-cases')).expanduser()
CACHE_ROOT.mkdir(parents=True, exist_ok=True)

def download(url, filename):
    destination = CACHE_ROOT / filename
    if not destination.exists():
        request = urllib.request.Request(url, headers={'User-Agent': 'ai-course-case/1.0'})
        with urllib.request.urlopen(request, timeout=120) as response:
            with tempfile.NamedTemporaryFile(dir=CACHE_ROOT, delete=False) as tmp:
                shutil.copyfileobj(response, tmp)
                temporary = Path(tmp.name)
        temporary.replace(destination)
    digest = hashlib.sha256(destination.read_bytes()).hexdigest()
    print(f'文件:{destination.name};大小:{destination.stat().st_size / 1024**2:.2f} MiB;SHA-256:{digest}')
    return destination

plt.rcParams['figure.dpi'] = 120
print('数据缓存:', CACHE_ROOT)
数据缓存: /private/tmp/ai-course-case-data

第 1 步:读取原始文件并确定输入、输出#

  压缩包中的 wdbc.data 没有表头。第 1 列是记录标识符,第 2 列是标签,后面 30 列才是模型输入。标识符只用于识别记录,不应被模型当作可泛化的测量特征。读取后立即核对列数、标签集合、缺失值和重复标识符,避免把格式错误带进网络。

URL = 'https://archive.ics.uci.edu/static/public/17/breast+cancer+wisconsin+diagnostic.zip'
archive = download(URL, 'uci_wdbc.zip')
base = ['radius', 'texture', 'perimeter', 'area', 'smoothness', 'compactness', 'concavity', 'concave_points', 'symmetry', 'fractal_dimension']
feature_names = [f'{name}_{group}' for group in ('mean', 'se', 'worst') for name in base]
columns = ['id', 'label', *feature_names]
with zipfile.ZipFile(archive) as zf:
    members = [name for name in zf.namelist() if name.lower().endswith('wdbc.data')]
    if len(members) != 1:
        raise RuntimeError(f'未唯一找到 wdbc.data:{members}')
    raw = pd.read_csv(io.BytesIO(zf.read(members[0])), header=None, names=columns)

assert raw.shape[1] == 32
assert set(raw['label']) == {'M', 'B'}
assert raw['id'].is_unique
assert not raw.isna().any().any()
print('原始数据维度:', raw.shape)
preview_columns = {'label': '标签', 'radius_mean': '平均半径', 'texture_mean': '平均纹理', 'perimeter_mean': '平均周长'}
display(raw[list(preview_columns)].head(3).rename(columns=preview_columns))
文件:uci_wdbc.zip;大小:0.05 MiB;SHA-256:bc154869ef13f753f9e2b5a17e248cfe1ba4b6721db7c4da9f4880e40b05d3af
原始数据维度: (569, 32)
标签 平均半径 平均纹理 平均周长
0 M 17.99 10.38 122.8
1 M 20.57 17.77 132.9
2 M 19.69 21.25 130.0

第 2 步:检查类别与特征#

  先查看各类别的样本数量与特征的数值尺度。面积、周长和光滑度不在同一尺度;若直接训练,数值较大的特征可能对梯度产生过强影响。许多特征之间也具有较高相关性,因此深层网络未必优于线性模型,后面需要保留逻辑回归作为简单基线。

  检查数据时不使用测试集确定删除规则。极端测量值可能正是较难预测但有意义的样本,不能为了让图形或指标更好看而随意删除。

feature_display_names = dict(zip(feature_names, [
    f'{group}{name}'
    for group in ('均值-', '标准误-', '最差值-')
    for name in ('半径', '纹理', '周长', '面积', '光滑度', '紧致度', '凹度', '凹点数', '对称性', '分形维数')
]))
summary = raw[feature_names].describe().T[['mean', 'std', 'min', 'max']]
display(summary.rename(index=feature_display_names, columns={'mean': '均值', 'std': '标准差', 'min': '最小值', 'max': '最大值'}).head(10))
class_counts = raw['label'].value_counts().sort_index()
class_counts.plot.bar(title='M/B 标签数量')
plt.xlabel('标签')
plt.ylabel('样本数')
plt.show()
print('标签比例:', (class_counts / class_counts.sum()).round(3).to_dict())
print('绝对相关系数最高的若干特征对说明输入存在冗余;这不是读取错误。')
corr = raw[feature_names].corr().abs()
upper = corr.where(np.triu(np.ones(corr.shape), 1).astype(bool))
top_correlations = upper.stack().sort_values(ascending=False).head(5).reset_index()
top_correlations.columns = ['特征一', '特征二', '绝对相关系数']
top_correlations['特征一'] = top_correlations['特征一'].replace(feature_display_names)
top_correlations['特征二'] = top_correlations['特征二'].replace(feature_display_names)
display(top_correlations)
均值 标准差 最小值 最大值
均值-半径 14.127292 3.524049 6.98100 28.11000
均值-纹理 19.289649 4.301036 9.71000 39.28000
均值-周长 91.969033 24.298981 43.79000 188.50000
均值-面积 654.889104 351.914129 143.50000 2501.00000
均值-光滑度 0.096360 0.014064 0.05263 0.16340
均值-紧致度 0.104341 0.052813 0.01938 0.34540
均值-凹度 0.088799 0.079720 0.00000 0.42680
均值-凹点数 0.048919 0.038803 0.00000 0.20120
均值-对称性 0.181162 0.027414 0.10600 0.30400
均值-分形维数 0.062798 0.007060 0.04996 0.09744
../../_images/72c135819193de56b3a6dfbed5a0d1945dd076acd44d80da2d0034d61bc0f621.png
标签比例: {'B': 0.627, 'M': 0.373}
绝对相关系数最高的若干特征对说明输入存在冗余;这不是读取错误。
特征一 特征二 绝对相关系数
0 均值-半径 均值-周长 0.997855
1 最差值-半径 最差值-周长 0.993708
2 均值-半径 均值-面积 0.987357
3 均值-周长 均值-面积 0.986507
4 最差值-半径 最差值-面积 0.984015

第 3 步:划分数据并进行标准化#

  先按标签比例把数据划分为训练集、验证集和测试集,再只用训练集计算均值与标准差。验证集用于选择阈值和训练轮数;测试集只在模型与训练方案确定后使用。观测标签写成 \(n\times1\) 的二维列向量,偏置写成 \(1\times d_l\) 的行向量。当

\[\boldsymbol{Z}^{[l]}=\boldsymbol{A}^{[l-1]}\cdot\boldsymbol{W}^{[l]}+\boldsymbol{b}^{[l]},\]

  NumPy 会利用广播把同一个偏置行向量加到每个样本上,而不会真的保存 \(n\) 份相同参数。

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

X = raw[feature_names].to_numpy(np.float64)
y = (raw[['label']].to_numpy() == 'M').astype(np.float64)
X_train, X_hold, y_train, y_hold = train_test_split(X, y, test_size=0.40, stratify=y, random_state=SEED)
X_val, X_test, y_val, y_test = train_test_split(X_hold, y_hold, test_size=0.50, stratify=y_hold, random_state=SEED)
scaler = StandardScaler().fit(X_train)
X_train_z = scaler.transform(X_train)
X_val_z = scaler.transform(X_val)
X_test_z = scaler.transform(X_test)

assert X_train_z.shape == (y_train.shape[0], len(feature_names))
assert X_val_z.shape[0] == y_val.shape[0]
assert X_test_z.shape[0] == y_test.shape[0]
assert y_train.ndim == 2 and y_train.shape[1] == 1
assert np.allclose(X_train_z.mean(axis=0), 0, atol=1e-12)
print('训练/验证/测试:', X_train_z.shape, X_val_z.shape, X_test_z.shape)
训练/验证/测试: (341, 30) (114, 30) (114, 30)

第 4 步:建立简单基线#

  逻辑回归没有隐藏层,其得分是输入的线性组合。它回答一个关键问题:深层网络带来的非线性是否真的改善了未见数据表现?基线与神经网络使用完全相同的训练划分和训练集标准化器。此处只在验证集查看结果,不提前读取测试指标。

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, log_loss, roc_auc_score

baseline = LogisticRegression(max_iter=2000, random_state=SEED).fit(X_train_z, y_train.ravel())
base_val_prob = baseline.predict_proba(X_val_z)[:, 1]
print({'验证对数损失': log_loss(y_val.ravel(), base_val_prob), '验证 ROC 曲线下面积': roc_auc_score(y_val.ravel(), base_val_prob)})
{'验证对数损失': 0.054993087588014855, '验证 ROC 曲线下面积': 1.0}

第 5 步:把公式写成前向传播程序#

  各层神经元数量取 \([d_0,16,8,1]\)批量样本按行存放,因此 \(\boldsymbol{W}^{[l]}\in\mathbb{R}^{d_{l-1}\times d_l}\)\(\boldsymbol{b}^{[l]}\in\mathbb{R}^{1\times d_l}\)隐藏层使用 ReLU,输出层使用 sigmoid 函数。

  每层保存上一层的激活值与本层的线性运算结果。后向传播需要复用这些中间量;如果只保存最终概率,就无法逐层应用链式法则。He 初始化使隐藏层权重的标准差约为 \(\sqrt{2/d_{l-1}}\)用来降低 ReLU 网络中的信号随着层数增加而过快缩小或放大的风险。

def sigmoid(z):
    out = np.empty_like(z, dtype=np.float64)
    positive = z >= 0
    out[positive] = 1.0 / (1.0 + np.exp(-z[positive]))
    exp_z = np.exp(z[~positive])
    out[~positive] = exp_z / (1.0 + exp_z)
    return out

def initialize(dims, seed=SEED):
    local_rng = np.random.default_rng(seed)
    params = {}
    for layer in range(len(dims) - 1):
        params[f'W{layer}'] = local_rng.normal(0, np.sqrt(2 / dims[layer]), size=(dims[layer], dims[layer + 1]))
        params[f'b{layer}'] = np.zeros((1, dims[layer + 1]))
    return params

def forward(X_batch, params):
    A = X_batch
    caches = []
    last = len(params) // 2 - 1
    for layer in range(last):
        Z = A @ params[f'W{layer}'] + params[f'b{layer}']
        caches.append((A, Z))
        A = np.maximum(Z, 0)
    logits = A @ params[f'W{last}'] + params[f'b{last}']
    caches.append((A, logits))
    return sigmoid(logits), caches

def bce(probability, target):
    p = np.clip(probability, 1e-12, 1 - 1e-12)
    return float(-np.mean(target * np.log(p) + (1 - target) * np.log(1 - p)))

params = initialize([X_train_z.shape[1], 16, 8, 1])
probe, caches = forward(X_train_z[:5], params)
assert probe.shape == (5, 1)
assert caches[0][0].shape == (5, 30) and caches[0][1].shape == (5, 16)
assert caches[-1][1].shape == (5, 1)
print('五个样本的输出维度:', probe.shape)
五个样本的输出维度: (5, 1)

第 6 步:误差信号与手工后向传播#

  输出层同时使用 sigmoid 函数与二元交叉熵时,误差信号可以简化为 \(\mathrm{d}\boldsymbol{Z}^{[L]}=(\widehat{\boldsymbol{y}}-\boldsymbol{y})/n\)随后从输出层向输入层依次计算

\[\mathrm{d}\boldsymbol{W}^{[l]}=(\boldsymbol{A}^{[l-1]})^{\mathsf T}\cdot\mathrm{d}\boldsymbol{Z}^{[l]},\qquad \mathrm{d}\boldsymbol{b}^{[l]}=\sum_i\mathrm{d}\boldsymbol{Z}^{[l]}_i.\]

  ReLU 在线性运算结果大于 0 时导数为 1,小于 0 时导数为 0。代码中每个梯度的维度必须与对应参数完全相同;否则,即使广播使程序能够运行,参数更新的含义也可能错误。

def backward(probability, target, caches, params, l2=0.0):
    n = target.shape[0]
    grads = {}
    last = len(caches) - 1
    dZ = (probability - target) / n
    for layer in range(last, -1, -1):
        A_previous, Z = caches[layer]
        W = params[f'W{layer}']
        grads[f'W{layer}'] = A_previous.T @ dZ + (l2 / n) * W
        grads[f'b{layer}'] = dZ.sum(axis=0, keepdims=True)
        if layer > 0:
            dA_previous = dZ @ W.T
            previous_Z = caches[layer - 1][1]
            dZ = dA_previous * (previous_Z > 0)
    for key, gradient in grads.items():
        assert gradient.shape == params[key].shape, (key, gradient.shape, params[key].shape)
    return grads

small_probability, small_cache = forward(X_train_z[:8], params)
grads = backward(small_probability, y_train[:8], small_cache, params)
display(pd.DataFrame([{'参数': key, '梯度维度': str(value.shape)} for key, value in grads.items()]))
参数 梯度维度
0 W2 (8, 1)
1 b2 (1, 1)
2 W1 (16, 8)
3 b1 (1, 8)
4 W0 (30, 16)
5 b0 (1, 16)

第 7 步:数值梯度检查#

  手工编写后向传播时,容易出现转置、样本平均因子或广播方面的错误。训练前可以对少量参数使用中心差分:把一个参数分别增加和减少一个很小的正数 \(\epsilon\)再根据两次损失之差近似该参数的梯度,并与解析梯度比较。抽查少量参数通常就能发现许多具有共同原因的错误;数值梯度计算很慢,不用于正式训练。ReLU 在 0 处不可导,因此遇到非常接近折点的元素时,可以更换样本或选择其他参数进行检查。

def gradient_check(X_small, y_small, params, epsilon=1e-5):
    probability, cache = forward(X_small, params)
    analytic = backward(probability, y_small, cache, params)
    checks = []
    positions = [('W0', (0, 0)), ('W0', (3, 5)), ('b0', (0, 2)), ('W1', (1, 3)), ('b1', (0, 0)), ('W2', (2, 0)), ('b2', (0, 0))]
    for key, index in positions:
        original = params[key][index]
        params[key][index] = original + epsilon
        plus = bce(forward(X_small, params)[0], y_small)
        params[key][index] = original - epsilon
        minus = bce(forward(X_small, params)[0], y_small)
        params[key][index] = original
        numerical = (plus - minus) / (2 * epsilon)
        exact = analytic[key][index]
        relative_error = abs(numerical - exact) / max(1e-10, abs(numerical) + abs(exact))
        checks.append((key, index, exact, numerical, relative_error))
    return pd.DataFrame(checks, columns=['参数', '位置', '解析梯度', '数值梯度', '相对误差'])

check_table = gradient_check(X_train_z[:8], y_train[:8], params)
display(check_table)
assert check_table['相对误差'].max() < 1e-4
参数 位置 解析梯度 数值梯度 相对误差
0 W0 (0, 0) -0.022937 -0.022937 1.018147e-11
1 W0 (3, 5) 0.013202 0.013202 1.852271e-10
2 b0 (0, 2) 0.002241 0.002241 6.441441e-10
3 W1 (1, 3) 0.002678 0.002678 2.835485e-10
4 b1 (0, 0) -0.136502 -0.136502 1.912046e-11
5 W2 (2, 0) 0.146318 0.146318 2.557195e-11
6 b2 (0, 0) 0.083400 0.083400 4.408782e-12

第 8 步:小批量训练与验证曲线#

  训练循环每次只用一批样本估计梯度。固定随机种子使初始化和批次顺序可复现。训练损失反映拟合能力,验证损失反映当前参数对未用于更新的数据的表现;二者分离才能识别欠拟合或过拟合。

  这里使用较小网络和固定 600 轮,运行时间通常只有数秒至数分钟。L2 只进入训练目标;图上报告的数据损失不含惩罚项,便于比较预测质量。

def train_network(X_train, y_train, X_validation, y_validation, dims, epochs=600, learning_rate=0.02, batch_size=64, l2=1e-3):
    params = initialize(dims, seed=SEED)
    local_rng = np.random.default_rng(SEED)
    history = []
    for epoch in range(epochs + 1):
        if epoch > 0:
            order = local_rng.permutation(len(X_train))
            for start in range(0, len(order), batch_size):
                index = order[start:start + batch_size]
                probability, cache = forward(X_train[index], params)
                grads = backward(probability, y_train[index], cache, params, l2=l2)
                for key in params:
                    params[key] -= learning_rate * grads[key]
        if epoch % 20 == 0:
            train_probability = forward(X_train, params)[0]
            validation_probability = forward(X_validation, params)[0]
            history.append({'epoch': epoch, 'train_bce': bce(train_probability, y_train), 'validation_bce': bce(validation_probability, y_validation)})
    return params, pd.DataFrame(history)

network, history = train_network(X_train_z, y_train, X_val_z, y_val, [30, 16, 8, 1])
history_display = history.rename(columns={'epoch': '训练轮次', 'train_bce': '训练二元交叉熵', 'validation_bce': '验证二元交叉熵'})
display(history_display.tail())
history_display.plot(x='训练轮次', y=['训练二元交叉熵', '验证二元交叉熵'])
plt.xlabel('训练轮次')
plt.ylabel('二元交叉熵')
plt.title('训练与验证曲线')
plt.show()
assert np.isfinite(history[['train_bce', 'validation_bce']].to_numpy()).all()
训练轮次 训练二元交叉熵 验证二元交叉熵
26 520 0.017273 0.057180
27 540 0.016360 0.057353
28 560 0.015490 0.056722
29 580 0.014683 0.057045
30 600 0.013880 0.057537
../../_images/e87f152fe2d06c213c28a0a039ed71dd89c2437770aed3ea59aa0f8b0d91cc3a.png

第 9 步:确定模型与阈值后评价测试集#

  阈值也是模型方案的一部分,必须在验证集上选择。这里采用“使验证集 F1 分数最大”的教学规则;若实际任务中漏报和误报的代价不同,应事先写出两类错误的代价,并选择使验证集总代价最小的阈值。确定阈值和网络后,逻辑回归与全连接网络才各自在测试集上评价一次。

  同时报告交叉熵、ROC 曲线下面积与固定阈值指标:交叉熵评价概率预测,ROC 曲线下面积评价排序能力,准确率和 F1 分数评价给定阈值下的分类结果。它们回答的问题不同,不能相互替代。

from sklearn.metrics import confusion_matrix, f1_score

val_probability = forward(X_val_z, network)[0].ravel()
candidates = np.linspace(0.05, 0.95, 181)
val_f1 = [f1_score(y_val.ravel(), val_probability >= threshold) for threshold in candidates]
chosen_threshold = float(candidates[int(np.argmax(val_f1))])

def metric_row(name, probability, threshold):
    prediction = probability >= threshold
    return {'model': name, 'threshold': threshold, 'log_loss': log_loss(y_test.ravel(), probability), 'roc_auc': roc_auc_score(y_test.ravel(), probability), 'accuracy': accuracy_score(y_test.ravel(), prediction), 'f1': f1_score(y_test.ravel(), prediction)}

network_test_probability = forward(X_test_z, network)[0].ravel()
baseline_test_probability = baseline.predict_proba(X_test_z)[:, 1]
results = pd.DataFrame([
    metric_row('NumPy 全连接网络', network_test_probability, chosen_threshold),
    metric_row('逻辑回归基线', baseline_test_probability, 0.5),
]).rename(columns={'model': '模型', 'threshold': '阈值', 'log_loss': '对数损失', 'roc_auc': 'ROC 曲线下面积', 'accuracy': '准确率', 'f1': 'F1 分数'})
display(results)
network_test_prediction = network_test_probability >= chosen_threshold
print('全连接网络混淆矩阵,行是真实标签 0/1,列是预测标签 0/1:')
display(pd.DataFrame(confusion_matrix(y_test.ravel(), network_test_prediction), index=['真实_0', '真实_1'], columns=['预测_0', '预测_1']))
模型 阈值 对数损失 ROC 曲线下面积 准确率 F1 分数
0 NumPy 全连接网络 0.06 0.153706 0.977513 0.947368 0.930233
1 逻辑回归基线 0.50 0.086141 0.994048 0.964912 0.950000
全连接网络混淆矩阵,行是真实标签 0/1,列是预测标签 0/1:
预测_0 预测_1
真实_0 68 4
真实_1 2 40

第 10 步:错误分析#

  整体指标不能说明模型错在什么地方。下面找出置信度最高的错误,并查看若干原始测量。模型确信程度用 \(|\widehat p-0.5|\) 表示;它只是模型内部信心,不等于医学可信度。错误分析只能帮助提出后续检查问题,不能依据少量样本事后修改测试集或标签。

error_mask = network_test_prediction.astype(int) != y_test.ravel().astype(int)
error_table = pd.DataFrame(X_test, columns=feature_names)
error_table['true_label'] = y_test.ravel().astype(int)
error_table['probability_M'] = network_test_probability
error_table['predicted_label'] = network_test_prediction.astype(int)
error_table['confidence'] = np.abs(network_test_probability - 0.5)
most_confident_errors = error_table.loc[error_mask].sort_values('confidence', ascending=False)
print('测试错误数:', int(error_mask.sum()))
display(most_confident_errors[['true_label', 'predicted_label', 'probability_M', 'radius_mean', 'texture_mean', 'concavity_worst']].head(8).rename(columns={'true_label': '真实标签', 'predicted_label': '预测标签', 'probability_M': 'M 类概率', 'radius_mean': '平均半径', 'texture_mean': '平均纹理', 'concavity_worst': '最差凹度'}))
测试错误数: 6
真实标签 预测标签 M 类概率 平均半径 平均纹理 最差凹度
65 1 0 0.000459 13.80 15.79 0.2779
26 1 0 0.002080 14.22 23.12 0.8488
93 0 1 0.063578 11.75 17.56 0.1366
42 0 1 0.064996 15.10 16.39 0.1960
79 0 1 0.211540 11.34 21.26 0.3120
7 0 1 0.284290 11.62 18.18 0.3186

结论、局限与常见错误#

  • 手工实现揭示了缓存和误差信号如何连接每一层;数值梯度检查比“程序能运行”更有说服力。

  • 深层网络不保证优于逻辑回归。样本量小、特征已经高度工程化时,简单模型可能更稳定。

  • 本案例只比较一次固定网络;一次随机种子的差异不能证明某结构普遍更好。

  • 不要先在全数据上标准化、选轮数或选阈值;这会让验证或测试信息进入训练流程。

  • ReLU 的 0 点、概率裁剪和 L2 平均因子都可能造成梯度检查差异,应明确实现约定。

  • 数据来自特定采集流程,缺少外部机构验证、群体公平性和时间漂移分析,不能用于任何真实医疗决策。

综合练习#

  1. 将网络改成一个隐藏层,逐层列出 \(\boldsymbol{A}\)\(\boldsymbol{W}\)\(\boldsymbol{b}\) 和误差信号的维度,并核对参数总数。

  2. 在其他条件相同的情况下,只把隐藏层神经元数量改为 4、16 和 64;使用多个随机种子,报告各模型验证指标的均值与标准差,并分析隐藏层神经元数量可能带来的影响。

  3. 分别使用 sigmoid、tanh 和 ReLU 作为隐藏层激活函数,记录每层激活接近饱和或等于 0 的比例。

  4. 为各层抽取若干权重进行数值梯度检查,并解释为什么不应在 ReLU 的折点要求解析梯度与数值梯度高度一致。

  5. 只用验证集比较阈值 0.5、使 F1 分数最大的阈值和使给定误判代价最小的阈值;确定规则后,再评价一次测试集。

  6. 用自动微分框架建立维度相同的网络,对同一个批次比较损失与梯度;说明参数初始化、损失取平均的方式和正类标签必须完全一致。