AI: 无监督学习

最后更新:2026-08-26

数据没有标签怎么办?当你面对一堆用户行为记录、基因序列或传感器数据,却没有任何"正确答案"时,无监督学习就是你发现隐藏结构的利器。本章从聚类和降维两大方向出发,带你理解 K-Means 和 PCA 的原理与实战。

1. 你将学到


2. 故事:100K Users,谁来帮他们分组?

(1) 痛点:海量用户无从下手

Charlie 的营销团队有 100K user data——消费金额、购买频率、注册时长、最近活跃时间……但没人知道用户能分成几类。市场部凭着经验把用户分为"新用户"和"老用户"两组推送优惠券,结果转化率只有 2.3%。CEO 要求提升到 5%,团队一筹莫展。

(2) AI 的解法:用聚类自动发现用户群

Alice 接手后,用 K-Means 把用户聚成了 4 群:

群编号 特征 占比 命名
群 0 高消费低频 15% 大额买家
群 1 低消费高频 35% 活跃羊毛党
群 2 高消费高频 10% 核心VIP
群 3 低消费低频 40% 沉默用户

每个群推送不同的优惠券:给大额买家推荐新品、给活跃羊毛党发满减券、给核心 VIP 发专属权益、给沉默用户发唤醒红包。转化率直接从 2.3% 提升到 3.1%,提升了 35%。

(3) 收益:从"拍脑袋"到"数据驱动"

Charlie 发现:无监督学习的威力不在于预测,而在于发现你不知道的存在。没有标签、没有先验知识,算法依然能从数据中挖掘出有意义的分组——这就是无监督学习的价值。


3. 无监督学习 vs 监督学习

(1) 核心区别

维度 监督学习 无监督学习
训练数据 有标签(X → y) 无标签(只有 X)
目标 预测已知类别/数值 发现数据内在结构
典型任务 分类、回归 聚类、降维、关联规则
评估方式 准确率、F1、RMSE 等 轮廓系数、惯性、可视化
类比 老师教学生认动物 孩子自己把玩具按颜色分类
代表算法 决策树、SVM、线性回归 K-Means、PCA、DBSCAN

(2) 为什么需要无监督学习


4. K-Means 聚类

(1) 什么是聚类

聚类(Clustering)是把相似的数据点分到同一组、不相似的分到不同组的过程。关键问题:什么叫"相似"?——通常用距离度量,最常见的是欧氏距离。

(2) K-Means 算法流程

K-Means 是最经典、最实用的聚类算法。核心思想:把数据分成 k 个组,让每个点到自己组中心的距离之和最小

100%
flowchart TD
    A[Initialize k centroids randomly] --> B[Assign each point to nearest centroid]
    B --> C[Recalculate centroids as mean of assigned points]
    C --> D{Centroids changed?}
    D -- Yes --> B
    D -- No --> E[Converged! Output clusters]
    
    style A fill:#e1f5fe
    style E fill:#e8f5e9
    style D fill:#fff3e0

算法步骤:

  1. 初始化:随机选择 k 个点作为初始质心(centroid)
  2. 分配:将每个数据点分配到最近的质心所在的簇
  3. 更新:重新计算每个簇的质心(取簇内所有点的均值)
  4. 迭代:重复步骤 2-3,直到质心不再变化或达到最大迭代次数

▶ 示例:K-Means 聚类客户数据(难度⭐)

PYTHON
from sklearn.cluster import KMeans
import numpy as np

np.random.seed(42)

group_a = np.random.normal(loc=[20, 80], scale=5, size=(30, 2))
group_b = np.random.normal(loc=[80, 20], scale=5, size=(30, 2))
group_c = np.random.normal(loc=[50, 50], scale=5, size=(30, 2))
X = np.vstack([group_a, group_b, group_c])

kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)

print(f"Cluster centers:\n{kmeans.cluster_centers_}")
print(f"Inertia (sum of squared distances): {kmeans.inertia_:.2f}")
print(f"First 10 labels: {kmeans.labels_[:10]}")
💻 输出:

TEXT 📖 仅展示
Cluster centers:
[[50.14 49.60]
 [20.38 79.63]
 [80.27 19.44]]
Inertia (sum of squared distances): 1310.56
First 10 labels: [1 1 1 1 1 1 1 1 1 1]

(3) 三种聚类算法对比

维度 K-Means 层次聚类 DBSCAN
需要指定 k 否(需指定 eps/min_samples)
簇形状 球形 任意 任意
处理噪声 一般 好(标记为噪声点)
时间复杂度 O(nkt) O(n²) ~ O(n³) O(n log n)
可扩展性 大数据集友好 大数据集慢 中等
适用场景 客户分群、图像压缩 小数据集、树状结构 异常检测、空间数据

5. 如何选择 k 值

K-Means 最大的问题:k 需要你提前指定。选错了 k,聚类结果就没意义。两种常用方法帮你找到最优 k。

(1) 肘部法则(Elbow Method)

绘制不同 k 值对应的惯性(inertia,即簇内平方距离和)曲线,选择"拐弯处"的 k——就像手臂的肘部,之后增加 k 收益递减。

▶ 示例:肘部法则画图选 k(难度⭐)

PYTHON
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
import numpy as np

np.random.seed(42)
X = np.vstack([
    np.random.normal(loc=[20, 80], scale=5, size=(50, 2)),
    np.random.normal(loc=[80, 20], scale=5, size=(50, 2)),
    np.random.normal(loc=[50, 50], scale=5, size=(50, 2)),
    np.random.normal(loc=[80, 80], scale=5, size=(50, 2)),
])

inertias = []
K_range = range(1, 11)
for k in K_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X)
    inertias.append(km.inertia_)

plt.figure(figsize=(8, 5))
plt.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8)
plt.xlabel('Number of clusters (k)')
plt.ylabel('Inertia')
plt.title('Elbow Method for Optimal k')
plt.axvline(x=4, color='red', linestyle='--', label='Elbow at k=4')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('elbow_method.png', dpi=150)
plt.show()
💡 提示: 肘部法则看的是惯性下降的"拐点"。k=4 之后惯性下降明显变缓,说明 4 个簇是合理的。

(2) Silhouette 评分

Silhouette(轮廓系数)衡量每个点与自己簇的相似度 vs 与最近其他簇的相似度,取值 [-1, 1],越大越好。

▶ 示例:Silhouette 评分选择 k(难度⭐⭐)

PYTHON
from sklearn.metrics import silhouette_score
from sklearn.cluster import KMeans
import numpy as np

np.random.seed(42)
X = np.vstack([
    np.random.normal(loc=[20, 80], scale=5, size=(50, 2)),
    np.random.normal(loc=[80, 20], scale=5, size=(50, 2)),
    np.random.normal(loc=[50, 50], scale=5, size=(50, 2)),
    np.random.normal(loc=[80, 80], scale=5, size=(50, 2)),
])

print("k | Silhouette Score")
print("--|------------------")
for k in range(2, 8):
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = km.fit_predict(X)
    score = silhouette_score(X, labels)
    print(f"{k} | {score:.4f}")
💻 输出:

TEXT 📖 仅展示
k | Silhouette Score
--|------------------
2 | 0.5812
3 | 0.6234
4 | 0.6891
5 | 0.5543
6 | 0.4672
7 | 0.3891
💡 提示: k=4 时 Silhouette 评分最高,与肘部法则结论一致。两种方法交叉验证更可靠。

(3) 聚类评估方法对比

方法 衡量内容 取值范围 优点 缺点
Inertia 簇内紧密度 ≥0,越小越好 计算快,K-Means 自带 随 k 增大单调下降,不能单独用
Silhouette 紧密度 + 分离度 [-1, 1],越大越好 可比较不同 k 计算慢,O(n²)
Calinski-Harabasz 簇间/簇内方差比 ≥0,越大越好 计算快 对凸形簇偏好
Davies-Bouldin 簇内散度/簇间距离比 ≥0,越小越好 直观 对凸形簇偏好

6. PCA 降维

(1) 为什么需要降维

(2) PCA 直觉

PCA(主成分分析)的核心思想:找到数据方差最大的方向,投影到这些方向上。方差大 = 信息多,投影后尽量保留原始信息。

想象一堆 3D 数据点呈"扁平"的椭球状——PCA 会找到最长的轴(方差最大),把数据投影到这个轴上,3D 就变成了 1D,同时尽量不丢失信息。

▶ 示例:PCA 降维到 2D 并可视化(难度⭐⭐)

PYTHON
from sklearn.decomposition import PCA
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
import numpy as np

iris = load_iris()
X = iris.data
y = iris.target

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

print(f"Original shape: {X.shape}")
print(f"After PCA:      {X_pca.shape}")
print(f"Explained variance ratio: {pca.explained_variance_ratio_}")
print(f"Total variance explained: {pca.explained_variance_ratio_.sum():.4f}")

plt.figure(figsize=(8, 6))
for target, color, label in zip([0, 1, 2], ['red', 'blue', 'green'], iris.target_names):
    plt.scatter(X_pca[y == target, 0], X_pca[y == target, 1],
                c=color, label=label, alpha=0.7, edgecolors='k', s=60)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%} variance)')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%} variance)')
plt.title('PCA: Iris Dataset Reduced to 2D')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('pca_iris.png', dpi=150)
plt.show()
💻 输出:

TEXT 📖 仅展示
Original shape: (150, 4)
After PCA:      (150, 2)
Explained variance ratio: [0.9246 0.0530]
Total variance explained: 0.9776
💡 提示: 4 维降到 2 维,依然保留了 97.8% 的信息!前两个主成分就几乎"讲完了整个故事"。

(3) 降维方法对比

方法 类型 核心思想 保持全局结构 保持局部结构 可解释性 适用场景
PCA 线性 最大方差方向 高(特征权重可解释) 快速降维、去噪
t-SNE 非线性 保持邻域概率 可视化(2D/3D)
UMAP 非线性 拓扑结构保持 较好 可视化、大规模数据
Autoencoder 非线性 神经网络压缩 视结构 视结构 图像/文本降维

7. t-SNE 可视化简介

PCA 是线性降维,擅长保持全局结构但会"压扁"非线性关系。t-SNE(t-Distributed Stochastic Neighbor Embedding)专门为可视化设计,擅长保持局部邻域结构——相似的点在 2D 中仍然靠近。

▶ 示例:t-SNE 可视化高维数据(难度⭐⭐)

PYTHON
from sklearn.manifold import TSNE
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
import numpy as np

digits = load_digits()
X = digits.data
y = digits.target

print(f"Original shape: {X.shape}")  # 64-dimensional handwritten digits

tsne = TSNE(n_components=2, random_state=42, perplexity=30)
X_tsne = tsne.fit_transform(X)

plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='tab10',
                      alpha=0.7, edgecolors='k', s=30)
plt.colorbar(scatter, label='Digit Class')
plt.title('t-SNE: 64D Handwritten Digits → 2D')
plt.xlabel('t-SNE Dimension 1')
plt.ylabel('t-SNE Dimension 2')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('tsne_digits.png', dpi=150)
plt.show()
⚠️ 注意: t-SNE 只适合可视化,不适合作为下游模型的预处理步骤。它不可复现(不同运行结果不同)、不保持全局距离、计算慢 O(n²)。生产环境降维用 PCA 或 UMAP。


8. 聚类与降维的实际应用

(1) 客户分群

电商/金融/游戏行业用聚类将用户分为不同群体,制定差异化策略:

行业 聚类特征 分群结果 业务动作
电商 消费金额、频率、品类偏好 高价值/低价值/流失风险 差异化优惠券
金融 收入、负债、信用记录 低风险/中等/高风险 差异化利率
游戏 在线时长、付费、社交活跃 鲸鱼/海豚/白嫖 差异化运营

(2) 图像压缩

K-Means 可用于图像颜色量化——把 millions of colors 聚成 k 种,大幅减小文件体积。

(3) 异常检测

不属于任何簇的点可能是异常值。DBSCAN 标记的噪声点、离质心很远的点都值得关注。

(4) 特征工程

PCA 降维后的主成分可作为新特征输入监督学习模型,去除噪声、减少共线性、加速训练。


9. 聚类结果与原始标签对比

有时你有标签但想看看无监督聚类能否"自己发现"这些类别——这是检验聚类质量的绝佳方式。

▶ 示例:聚类结果与原始标签对比(难度⭐⭐)

PYTHON
from sklearn.cluster import KMeans
from sklearn.datasets import load_iris
from sklearn.metrics import confusion_matrix, accuracy_score
from scipy.stats import mode
import numpy as np

iris = load_iris()
X = iris.data
y_true = iris.target

kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
y_pred = kmeans.fit_predict(X)

# Cluster labels are arbitrary, so we align them with true labels
aligned_pred = np.zeros_like(y_pred)
for i in range(3):
    mask = y_pred == i
    aligned_pred[mask] = mode(y_true[mask], keepdims=True).mode[0]

print("Confusion Matrix (cluster vs true label):")
print(confusion_matrix(y_true, aligned_pred))
print(f"\nAligned accuracy: {accuracy_score(y_true, aligned_pred):.4f}")
💻 输出:

TEXT 📖 仅展示
Confusion Matrix (cluster vs true label):
[[50  0  0]
 [ 0 48  2]
 [ 0 14 36]]

Aligned accuracy: 0.8933
💡 提示: 无监督聚类在不知道标签的情况下,自动达到了 89.3% 的分类准确率!这说明鸢尾花的三个品种在特征空间中确实有明显的分组结构。


10. 无监督学习的局限

局限 说明
结果无保证 没有标签,无法量化"对不对",只能靠业务判断
k 的选择主观 肘部法则的拐点可能不明显,不同方法结论可能不同
对初始化敏感 K-Means 不同的初始质心可能产生不同结果(需 n_init > 1)
只能发现凸形簇 K-Means 假设簇是球形的,环形/月牙形数据需用 DBSCAN
降维有信息损失 PCA 丢弃了方差小的维度,可能丢掉重要的细微差异
评估困难 没有"标准答案",评估依赖领域知识和可视化

11. 综合示例:Mall Customer 数据集客户分群

▶ 示例:数据驱动的客户分群完整流程(难度⭐⭐⭐)

PYTHON
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

# ============================================
# Step 1: Load and explore the Mall Customer dataset
# ============================================
np.random.seed(42)
n = 200
data = {
    'CustomerID': range(1, n + 1),
    'Gender': np.random.choice(['Male', 'Female'], n),
    'Age': np.random.randint(18, 70, n),
    'Annual_Income_kUSD': np.random.randint(15, 137, n),
    'Spending_Score': np.random.randint(1, 100, n),
}
df = pd.DataFrame(data)

# Inject realistic cluster structure
df.loc[:49, 'Annual_Income_kUSD'] = np.random.randint(15, 40, 50)
df.loc[:49, 'Spending_Score'] = np.random.randint(60, 100, 50)
df.loc[50:99, 'Annual_Income_kUSD'] = np.random.randint(70, 137, 50)
df.loc[50:99, 'Spending_Score'] = np.random.randint(60, 100, 50)
df.loc[100:149, 'Annual_Income_kUSD'] = np.random.randint(70, 137, 50)
df.loc[100:149, 'Spending_Score'] = np.random.randint(1, 40, 50)
df.loc[150:, 'Annual_Income_kUSD'] = np.random.randint(15, 40, 50)
df.loc[150:, 'Spending_Score'] = np.random.randint(1, 40, 50)

print("Dataset shape:", df.shape)
print(df.head(10))
print("\nDescriptive stats:")
print(df[['Age', 'Annual_Income_kUSD', 'Spending_Score']].describe())

# ============================================
# Step 2: Feature selection and scaling
# ============================================
features = ['Age', 'Annual_Income_kUSD', 'Spending_Score']
X = df[features].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# ============================================
# Step 3: Elbow method to find optimal k
# ============================================
inertias = []
silhouette_scores = []
K_range = range(2, 11)

for k in K_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = km.fit_predict(X_scaled)
    inertias.append(km.inertia_)
    silhouette_scores.append(silhouette_score(X_scaled, labels))

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(K_range, inertias, 'bo-', linewidth=2)
axes[0].set_xlabel('Number of clusters (k)')
axes[0].set_ylabel('Inertia')
axes[0].set_title('Elbow Method')
axes[0].axvline(x=4, color='red', linestyle='--', label='Elbow at k=4')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(K_range, silhouette_scores, 'go-', linewidth=2)
axes[1].set_xlabel('Number of clusters (k)')
axes[1].set_ylabel('Silhouette Score')
axes[1].set_title('Silhouette Method')
axes[1].axvline(x=4, color='red', linestyle='--', label='Best at k=4')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('mall_elbow_silhouette.png', dpi=150)
plt.show()

# ============================================
# Step 4: Final clustering with k=4
# ============================================
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
df['Cluster'] = kmeans.fit_predict(X_scaled)

# ============================================
# Step 5: Analyze each cluster
# ============================================
cluster_summary = df.groupby('Cluster')[features].mean()
print("\nCluster profiles (mean values):")
print(cluster_summary)

cluster_counts = df['Cluster'].value_counts().sort_index()
print("\nCluster sizes:")
print(cluster_counts)

# ============================================
# Step 6: PCA for 2D visualization
# ============================================
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

plt.figure(figsize=(10, 7))
colors = ['red', 'blue', 'green', 'orange']
labels_text = [
    'Cluster 0: Young High-Spenders',
    'Cluster 1: Rich High-Spenders',
    'Cluster 2: Rich Low-Spenders',
    'Cluster 3: Budget Low-Spenders',
]

for i in range(4):
    mask = df['Cluster'] == i
    plt.scatter(X_pca[mask, 0], X_pca[mask, 1],
                c=colors[i], label=labels_text[i],
                alpha=0.6, edgecolors='k', s=50)

centroids_pca = pca.transform(kmeans.cluster_centers_)
plt.scatter(centroids_pca[:, 0], centroids_pca[:, 1],
            c='black', marker='X', s=200, label='Centroids')

plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%})')
plt.title('Mall Customer Segmentation (K-Means + PCA)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('mall_clusters_pca.png', dpi=150)
plt.show()

# ============================================
# Step 7: Business recommendations
# ============================================
print("\n=== Business Recommendations ===")
for i in range(4):
    row = cluster_summary.loc[i]
    count = cluster_counts[i]
    print(f"\nCluster {i} ({count} customers):")
    print(f"  Avg Age: {row['Age']:.0f}, Income: ${row['Annual_Income_kUSD']:.0f}k, Spending: {row['Spending_Score']:.0f}/100")
💻 输出:

TEXT 📖 仅展示
Dataset shape: (200, 5)
   CustomerID  Gender  Age  Annual_Income_kUSD  Spending_Score
0           1  Female   56                  28              86
1           2    Male   25                  32              79
2           3  Female   38                  22              93
3           4  Female   36                  37              74
4           5    Male   47                  33              68
5           6  Female   32                  27              82
6           7  Female   51                  18              88
7           8    Male   19                  38              95
8           9    Male   23                  25              71
9          10  Female   27                  39              91

Descriptive stats:
              Age  Annual_Income_kUSD  Spending_Score
count  200.000000          200.000000      200.000000
mean    43.475000           60.720000       50.115000
std     15.441418           41.082376       31.494432
min     18.000000           15.000000         1.000000
max     69.000000          136.000000        99.000000

Cluster profiles (mean values):
             Age  Annual_Income_kUSD  Spending_Score
Cluster                                           
0        41.34           26.82           80.44
1        43.88           99.24           78.32
2        44.92          101.58           19.44
3        43.36           26.24           21.12

Cluster sizes:
0    50
1    50
2    50
3    50
Name: Cluster, dtype: int64

=== Business Recommendations ===

Cluster 0 (50 customers):
  Avg Age: 41, Income: $27k, Spending: 80/100

Cluster 1 (50 customers):
  Avg Age: 44, Income: $99k, Spending: 78/100

Cluster 2 (50 customers):
  Avg Age: 45, Income: $102k, Spending: 19/100

Cluster 3 (50 customers):
  Avg Age: 43, Income: $26k, Spending: 21/100

❓ 常见问题

Q 聚类结果怎么评估好坏?
A 没有标签时用内部指标:Silhouette 评分([-1,1],越大越好)、Calinski-Harabasz 指数(越大越好)、Davies-Bouldin 指数(越小越好)。有标签时可以和真实标签对比(对齐后算准确率)。最可靠的方式是业务验证——聚类结果是否符合业务直觉和可操作性。
Q K-Means 的 k 怎么选?
A 首选肘部法则看惯性拐点,再用 Silhouette 评分交叉验证。如果两种方法结论一致就放心用;如果不一致,优先考虑业务需求——4 个群比 8 个群更容易制定差异化策略。k 不是数学问题,是业务问题。
Q PCA 降维会丢失信息吗?
A 会,但"丢失"的不一定是坏信息。PCA 按方差从大到小排序,丢弃方差小的维度≈丢弃噪声。关键看保留了多少方差——通常 85% 以上即可。用 pca.explained_variance_ratio_ 查看每个主成分的贡献,再决定保留几个。
Q 无监督学习有什么实际用途?
A 五大典型场景:① 客户分群(营销个性化)② 异常检测(欺诈识别)③ 降维加速(特征工程)④ 图像压缩(颜色量化)⑤ 推荐系统冷启动(无行为数据时先聚类)。本质是"探索性分析"——先看数据长什么样,再决定做什么。
Q 聚类和分类哪个更难?
A 聚类更难,因为缺少"正确答案"。分类有标签,算准确率就知道好坏;聚类没有标签,评估靠内部指标和业务判断。而且 K-Means 对初始化敏感、k 选择主观、只适合凸形簇。分类是"有标准答案的考试",聚类是"没有标准答案的开放题"。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 sklearn.datasets.make_blobs 生成 3 簇数据,用 K-Means 聚类并画散点图(不同簇用不同颜色,标出质心)。

  2. 进阶题(难度⭐⭐):对上面的数据绘制肘部法则图和 Silhouette 评分图,确定最优 k。然后故意选 k=2 和 k=8 分别聚类,对比效果,解释为什么选错 k 结果会变差。

  3. 挑战题(难度⭐⭐⭐):用 sklearn.datasets.load_digits 加载手写数字数据集(64 维),先 PCA 降到 2D 可视化,再 t-SNE 降到 2D 可视化,对比两者的效果。写一段分析:PCA 和 t-SNE 各自保留了什么结构?哪个更适合这个数据集的可视化?

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏