玄同 765

大语言模型 (LLM) 开发工程师 | 中国传媒大学 · 数字媒体技术(智能交互与游戏设计)

CSDN · 个人主页 | GitHub · Follow


关于作者

  • 深耕领域:大语言模型开发 / RAG 知识库 / AI Agent 落地 / 模型微调
  • 技术栈:Python | RAG (LangChain / Dify + Milvus) | FastAPI + Docker
  • 工程能力:专注模型工程化部署、知识库构建与优化,擅长全流程解决方案

「让 AI 交互更智能,让技术落地更高效」
欢迎技术探讨与项目合作,解锁大模型与智能交互的无限可能!


从猜数游戏到模型训练:机器学习核心概念的无痛入门

引言:一个每个人都玩过的游戏

想象这样一个场景:你的朋友心里想了一个1到100之间的数字,你需要通过提问猜出这个数字。每次你猜一个数字,朋友只会告诉你"太大了"、“太小了"或者"猜对了”。你会怎么猜?

大多数人会采用一个看似简单却极其有效的策略:首先猜50(中间值)。如果朋友说"太小了",你就知道答案在51到100之间,下次猜75。如果朋友说"太大了",你就知道答案在1到49之间,下次猜25。

这个不断缩小范围、逐步逼近正确答案的过程,其实就是机器学习最核心的思想的直观体现。你在根据每一步的反馈(“太大"或"太小”)来更新你的判断(猜测的范围),最终找到最优解。

现在让我们把视线转向真实的商业场景。假设你是一家电商公司的数据科学家,老板让你预测下个月某款商品的销量,你会怎么做?

传统的方法是让经验丰富的分析师根据直觉和经验来估算。但这种方法有两个致命的缺陷:第一,人类无法处理像"过去三年每天每小时的网站访问量、用户点击流、社交媒体热度变化"这样海量的数据;第二,人类的判断容易受到情绪、疲劳、偏见等因素的影响。

机器学习提供了一种全新的解决方案:让计算机从历史数据中自动学习规律,然后用学到的规律来预测未来。这听起来很神奇,但背后的原理其实跟我们的猜数游戏一样简单——都是通过不断调整、逐步逼近最优解。

今天,让我们从零开始,用最通俗的语言和最直观的图解,系统性地理解机器学习最核心的概念。无论你是计算机专业的学生,还是其他领域想要入门AI的从业者,这一篇文章将为你打下坚实的基础。


零、前置知识:搭建理解这座大厦的地基

0.1 什么是函数?用做菜来理解

在讨论机器学习之前,我们需要先理解一个最基本的概念:函数。

想象你正在做一道番茄炒蛋。食谱上写着:"番茄2个,鸡蛋3个,盐5克,糖10克。“这个食谱实际上定义了一个"做菜函数”:输入是食材和调料,输出是一道菜。

用更数学的语言来说,函数就像是一个魔法盒子。你把一些东西放进去,它就会按照某种固定规则,吐出一个结果。这个"固定规则"就是函数的本质。

输出

函数 f(番茄,鸡蛋,盐,糖)

输入

番茄

鸡蛋

魔法盒子
固定规则

番茄炒蛋

在数学中,我们通常写作 y = f ( x ) y = f(x) y=f(x),其中 x x x 是输入, f f f 是函数规则, y y y 是输出。比如 f ( x ) = 2 x + 1 f(x) = 2x + 1 f(x)=2x+1,当你输入 x = 3 x=3 x=3 时,输出就是 y = 2 × 3 + 1 = 7 y = 2 \times 3 + 1 = 7 y=2×3+1=7

0.2 什么是求导?用开车来理解

导数是微积分的核心概念,理解它对于理解机器学习中的优化算法至关重要。

想象你正在高速公路上开车。仪表盘上有一个速度计,它告诉你现在开得有多快——这就是"瞬时速度"的概念。

但这里有一个更微妙的问题:如果我想知道我的速度"变化有多快",该怎么办?比如,我一脚油门踩下去,速度从60公里/小时飙升到120公里/小时,这个加速过程本身也有一个"加速度"。

导数描述的正是这样一个概念:当输入变化一点点时,输出会变化多少

速度与导数

导数 = 速度的变化率

加速过程

瞬时速度

0秒

1秒

1秒

反映加速程度

加速度
(导数)

60 km/h

80 km/h

100 km/h

120 km/h

在数学中,函数 f ( x ) f(x) f(x) 的导数记作 f ′ ( x ) f'(x) f(x) d f d x \frac{df}{dx} dxdf,它描述的是"当 x x x 变化一点点时, f ( x ) f(x) f(x) 如何变化"。如果 f ′ ( x ) > 0 f'(x) > 0 f(x)>0,说明随着 x x x 增加, f ( x ) f(x) f(x) 在增加;如果 f ′ ( x ) < 0 f'(x) < 0 f(x)<0,说明随着 x x x 增加, f ( x ) f(x) f(x) 在减少。

一个特别重要的例子是 f ( x ) = x 2 f(x) = x^2 f(x)=x2,它的导数是 f ′ ( x ) = 2 x f'(x) = 2x f(x)=2x。这意味着:

  • x = 1 x = 1 x=1 时, f ′ ( 1 ) = 2 f'(1) = 2 f(1)=2,说明如果 x x x 增加一点点, f ( x ) f(x) f(x) 会以大约2倍的速度增加
  • x = − 3 x = -3 x=3 时, f ′ ( − 3 ) = − 6 f'(-3) = -6 f(3)=6,说明如果 x x x 增加一点点, f ( x ) f(x) f(x) 会以大约6倍的速度减少

导数的符号告诉我们优化的方向:如果在某点 x x x f ′ ( x ) > 0 f'(x) > 0 f(x)>0,说明我们应该减小 x x x 来让 f ( x ) f(x) f(x) 变小;如果 f ′ ( x ) < 0 f'(x) < 0 f(x)<0,说明我们应该增大 x x x 来让 f ( x ) f(x) f(x) 变小。

0.3 什么是向量和矩阵?用坐标来理解

向量和矩阵是机器学习中无处不在的数学工具。让我用一个生活中的例子来解释。

假设你要描述一个位置,你可以说:"在向东走300米、向北走400米的地方。"这两个数字(300和400)就构成一个二维向量。

坐标系表示

走了

原点 (0,0)

点 (300,400)

向量:方向和大小的组合

方向:向东+向北

向量 v = (300, 400)

大小:500米

向量

在机器学习中,向量和矩阵让我们能够高效地表示和操作海量数据。比如,一张100×100像素的灰度图像可以表示为一个10000维的向量;如果是彩色图像,则需要3个这样的向量(对应RGB三个通道)。

矩阵则是向量的集合。假设我们有1000张这样的图像,把它们堆在一起,就形成了一个1000 × 10000的矩阵。矩阵运算让我们能够一次性对所有数据进行批量处理,这正是GPU擅长并行计算的任务类型。


一、机器学习到底是什么?

1.1 从传统编程到机器学习的范式转变

在传统编程中,我们需要明确地告诉计算机"怎么做"。比如,要写一个判断邮件是否为垃圾邮件的程序,程序员需要手动编写规则:

# 传统编程方式
def is_spam(email):
    # 手动编写规则
    if "免费" in email.text and "点击这里" in email.text:
        return True
    if email.sender in spam_senders_list:
        return True
    return False

这种方法的问题在于:规则需要人工设计,而且很难覆盖所有情况。垃圾邮件发送者会不断绕过规则,我们需要不断手动更新规则库。

机器学习采用了一种完全不同的思路:我们不给计算机规则,而是给它大量的例子,让它自己发现规则。

# 机器学习方式
# 1. 准备数据:大量邮件,每封标注是"垃圾"还是"正常"
emails = [...]  # 10000封邮件
labels = [...]  # 每封邮件的标签

# 2. 定义模型结构(不是具体规则,而是一个待调的函数)
model = NeuralNetwork()  # 或 LogisticRegression(), RandomForest() 等

# 3. 训练:让模型从数据中自动学习规则
model.fit(emails, labels)

# 4. 预测:直接使用学到的规则
prediction = model.predict(new_email)

机器学习范式

训练数据
(输入+正确答案)

自动学习规则
(调参过程)

模型架构
(待调函数)

新数据

输出预测结果

传统编程范式

人工设计规则

程序员手动编写

输入数据

执行固定规则

输出结果

这就是机器学习的核心哲学:不是告诉计算机怎么做,而是让它从数据中自己学会怎么做

1.2 机器学习的三种主要类型

根据学习方式的不同,机器学习可以分为三大类:监督学习、无监督学习和强化学习。

机器学习三大类型

强化学习 Reinforcement Learning

通过试错学习

最大化奖励信号

游戏AI、机器人控制

无监督学习 Unsupervised Learning

没有标签的数据

发现数据内部结构

客户分群、异常检测

监督学习 Supervised Learning

有标签的数据

学习输入到输出的映射

垃圾邮件分类、房价预测

监督学习是最常见的类型。想象你有一个导游,他带着你走过很多景点,同时告诉你每个景点叫什么名字(标签)。学习结束后,当你看到一个新的景点,你就知道该怎么称呼它了。监督学习就是这么回事:我们有"输入"和对应的"正确答案(标签)",模型学习从输入到标签的映射。

无监督学习则没有标签。想象你第一次去一个陌生的城市,没有人告诉你哪里是商业区、哪里是住宅区,但你通过观察人流量、店铺类型、建筑物密度等,自动发现城市自然地分成了几个区域。无监督学习就是在没有"正确答案"的情况下,让模型发现数据内在的结构。

强化学习则像是学习骑自行车。你会不断尝试,摔倒了很多次,但每次摔倒你都知道这是一个"坏结果"(负奖励)。当你终于能稳定骑行时,你收获了"好结果"(正奖励)。通过不断尝试和从结果中学习,你最终掌握了骑车技能。强化学习就是让智能体通过与环境交互、获得奖励或惩罚来学习最优策略。


二、监督学习:让模型从"有答案的练习"中学习

2.1 监督学习的核心流程

监督学习的过程可以类比为学生准备考试的过程。想象你要准备数学考试,你会:

  1. 做练习题:教材上的例题,每道题都有标准答案
  2. 对照答案:做完题后,和标准答案比对,看看自己哪里错了
  3. 理解错误:分析为什么会错,是粗心还是知识点没掌握
  4. 改进提高:下次遇到类似题目,争取做对

机器学习也是类似的流程:

监督学习完整流程

4. 最终预测

3. 模型验证

2. 模型训练

1. 准备训练数据

部署模型

收集数据

数据标注
(给数据贴标签)

喂入数据到模型

计算预测误差

调整模型参数

用验证集评估

调参优化

对新数据预测

让我把这个过程用具体的例子说得更清楚。假设我们要训练一个房价预测模型:

第一步:准备训练数据。我们需要收集大量房屋的信息,包括面积、位置、房龄、房间数等(这些称为特征,英文是features),以及每套房子的实际售价(这称为标签,英文是label)。比如:

面积(㎡) 位置 房龄(年) 房价(万)
100 市中心 5 500
80 郊区 10 300
120 市中心 3 650

第二步:训练模型。我们把数据喂给模型,模型会学习从特征到标签的映射关系。这个"学习"的过程,本质上是不断调整模型参数,让预测值和真实值越来越接近。

第三步:验证模型。用模型没有"见过"的数据(验证集)来测试模型表现。如果表现不好,就需要调整模型结构或训练策略。

第四步:预测新数据。模型训练好后,给它一间新房子的信息,它就能预测出这房子大概值多少钱。

2.2 损失函数:模型表现的"计分器"

在训练过程中,我们需要一个指标来衡量模型表现的好坏,这个指标就是损失函数(Loss Function)。

继续用考试的例子:如果一道题标准答案是"3",你答了"5",误差就是|5-3|=2;如果另一个人答了"4",误差就是|4-3|=1。显然第二个人的误差更小,表现更好。

在回归任务中,常用的损失函数是均方误差(Mean Squared Error,MSE):

M S E = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 MSE = \frac{1}{n} \sum_{i=1}^{n}(y_i - \hat{y}_i)^2 MSE=n1i=1n(yiy^i)2

这里的 y i y_i yi 是真实值, y ^ i \hat{y}_i y^i 是模型预测值。公式的意思就是把每个样本的预测误差平方后求平均。为什么要平方?因为误差可能是正也可能是负,平方后全是正数,可以直接相加。

MSE Calculation Process

Calculate Average

Squared Error

Three Samples Prediction

(25+400+100)/3=175

Sample1: Actual 100 Pred 95

(-5)^2=25

(-20)^2=400

10^2=100

Sample2: Actual 200 Pred 180

Sample3: Actual 150 Pred 160

对于分类任务,常用的损失函数是交叉熵(Cross Entropy)。直观地理解,交叉熵衡量的是"预测分布"和"真实分布"之间的差异。预测越接近真实标签,交叉熵越小。

2.3 梯度下降:让模型"学会"的核心算法

现在我们有了损失函数来衡量模型的好坏,接下来问题就是:如何调整模型参数,让损失最小化?

这就要用到梯度下降(Gradient Descent)算法了。

回想一下我们之前讲的导数概念。导数告诉我们,当输入增加一点点时,输出会怎么变化。如果损失函数 L L L 关于某个参数 w w w 的导数 ∂ L ∂ w > 0 \frac{\partial L}{\partial w} > 0 wL>0,说明增大 w w w 会让损失变大,所以我们应该减小 w w w;如果导数 < 0 < 0 <0,说明增大 w w w 会让损失变小,所以我们应该增大 w w w

梯度下降的核心思想就是:沿着梯度的反方向,一步一步地调整参数,逐步降低损失

梯度下降优化过程

第三次迭代

第二次迭代

第一次迭代

初始状态

计算梯度

参数更新

计算梯度

参数更新

计算梯度

参数更新

梯度 ∂L/∂w = 6.4

当前参数 w = 5.0

梯度 ∂L/∂w = 10

更新: w = 5.0 - 0.1×10 = 4.0

梯度 ∂L/∂w = 8

更新: w = 4.0 - 0.1×8 = 3.2

更新: w = 3.2 - 0.1×6.4 = 2.56

当前损失 = 25

新损失 = 16

新损失 = 10.24

新损失 = 6.55

在上面的例子中,学习率(Learning Rate)设为0.1,这是一个很重要的超参数。如果学习率太大,可能"步子迈得太大",一下子跳过了最优点;如果太小,收敛速度会很慢,需要很多次迭代才能到达最优点。

学习率对收敛的影响

学习率过小 (0.001)

步幅太小

收敛极慢

学习率过大 (1.0)

步幅过大

振荡或发散

学习率适中 (0.1)

步幅合适

稳定收敛

2.4 过拟合与欠拟合:模型复杂度的权衡

在机器学习中,有一个永恒的主题:如何平衡模型的复杂度和泛化能力?

欠拟合(Underfitting)就像一个学生太笨了,连教材上的例题都做不对。这通常发生在模型太简单、或者训练时间太短的时候。

过拟合(Overfitting)就像一个学生太"聪明"了,他把教材上的每一道题都背下来了,但遇到一道新题就傻眼了——因为它没有真正理解知识,只是记住了答案。这发生在模型太复杂、或者训练数据太少的时候。

从欠拟合到过拟合

过拟合:模型太复杂

特征: 极高次多项式

记住每一个噪声点

训练误差: 极低, 测试误差: 高

欠拟合:模型太简单

特征: y = 2x + 1

无法捕捉数据规律

训练误差: 高, 测试误差: 高

适度拟合:复杂度刚好

特征: 适当的非线性

很好地捕捉规律

训练误差: 低, 测试误差: 低

在实际工作中,我们如何解决这两个问题?

对于欠拟合,解决方案包括:增加模型复杂度、增加训练轮数、添加更多特征等。

对于过拟合,解决方案包括:使用更多训练数据、减少模型复杂度、添加正则化、使用Dropout、数据增强等。


三、用"猜价格"游戏完整演示机器学习流程

3.1 问题设定

让我用一个完整的例子来串联所有概念。假设你要开发一个房价预测系统,用户输入房子的基本信息,系统预测房价。

首先,我们收集了1000套房子的历史数据。每套房子有5个特征:面积、卧室数、浴室数、房龄、距市中心距离。目标是预测房价(单位:万元)。

3.2 数据准备

在实际项目中,数据准备往往占据80%的工作量。让我详细解释这个过程。

# 典型的数据处理流程
import pandas as pd
import numpy as np

# 1. 加载数据
df = pd.read_csv('house_prices.csv')
print(f"数据集大小: {df.shape}")  # (1000, 6) - 1000个样本,6列

# 2. 查看数据基本信息
print(df.head())  # 查看前几行
print(df.describe())  # 统计数据:均值、标准差、分位数等
print(df.isnull().sum())  # 缺失值统计

# 3. 处理缺失值
df['卧室数'].fillna(df['卧室数'].median(), inplace=True)  # 用中位数填充

# 4. 特征标准化(很重要!)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
features = ['面积', '卧室数', '浴室数', '房龄', '距市中心距离']
df[features] = scaler.fit_transform(df[features])

# 5. 划分训练集和测试集
from sklearn.model_selection import train_test_split
X = df[features]
y = df['房价']
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
# 80%用于训练,20%用于测试

3.3 模型选择与训练

现在我们有了处理好的数据,接下来选择模型并进行训练。

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

# 定义一个简单的神经网络模型
class HousePricePredictor(nn.Module):
    """
    房价预测神经网络
    
    输入: 5个房屋特征 (面积、卧室数、浴室数、房龄、距市中心距离)
    输出: 预测房价 (单值输出)
    
    架构说明:
    - 输入层: 5个神经元
    - 隐藏层1: 64个神经元 + ReLU激活
    - 隐藏层2: 32个神经元 + ReLU激活
    - 输出层: 1个神经元 (房价)
    """
    
    def __init__(self, input_size=5):
        super().__init__()
        # 第一层: 5 -> 64
        self.layer1 = nn.Linear(input_size, 64)
        # 第二层: 64 -> 32
        self.layer2 = nn.Linear(64, 32)
        # 输出层: 32 -> 1
        self.output = nn.Linear(32, 1)
        # ReLU激活函数,增加非线性
        self.relu = nn.ReLU()
    
    def forward(self, x):
        """
        前向传播
        
        数据流向:
        输入 x -> Layer1 -> ReLU -> Layer2 -> ReLU -> Output -> 输出房价
        """
        x = self.relu(self.layer1(x))
        x = self.relu(self.layer2(x))
        x = self.output(x)
        return x

# 创建模型实例
model = HousePricePredictor()
print(model)

# 定义损失函数和优化器
criterion = nn.MSELoss()  # 均方误差,用于回归任务
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# Adam优化器:自动调整学习率的梯度下降变体

3.4 训练循环详解

让我详细解释每一个训练步骤在做什么:

# 将数据转换为PyTorch张量
X_train_tensor = torch.FloatTensor(X_train.values)
y_train_tensor = torch.FloatTensor(y_train.values).reshape(-1, 1)
X_test_tensor = torch.FloatTensor(X_test.values)
y_test_tensor = torch.FloatTensor(y_test.values).reshape(-1, 1)

# 创建数据加载器,支持mini-batch训练
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
# batch_size=32: 每次喂32个样本给模型
# shuffle=True: 每个epoch前打乱数据顺序

# 训练循环
num_epochs = 100  # 训练轮数

for epoch in range(num_epochs):
    model.train()  # 设置为训练模式,启用dropout等
    epoch_loss = 0.0  # 累计损失
    
    for batch_idx, (features, labels) in enumerate(train_loader):
        # ===== 第一步:前向传播 =====
        # 把特征喂给模型,得到预测值
        predictions = model(features)
        
        # 计算预测值和真实值的误差
        loss = criterion(predictions, labels)
        
        # ===== 第二步:反向传播 =====
        # 先将梯度清零(否则会累加)
        optimizer.zero_grad()
        # 计算梯度
        loss.backward()
        # 更新参数
        optimizer.step()
        
        # 累计这一轮的损失
        epoch_loss += loss.item()
    
    # 每10轮打印一次
    if (epoch + 1) % 10 == 0:
        # 切换到评估模式
        model.eval()
        with torch.no_grad():
            # 在测试集上评估
            test_predictions = model(X_test_tensor)
            test_loss = criterion(test_predictions, y_test_tensor)
        
        print(f"Epoch [{epoch+1}/{num_epochs}], "
              f"Train Loss: {epoch_loss/len(train_loader):.4f}, "
              f"Test Loss: {test_loss.item():.4f}")

训练过程的输出会像这样:

Epoch [10/100], Train Loss: 245.3215, Test Loss: 312.4587
Epoch [20/100], Train Loss: 156.7842, Test Loss: 198.2341
Epoch [30/100], Train Loss: 98.4567, Test Loss: 125.6789
...
Epoch [100/100], Train Loss: 45.1234, Test Loss: 52.3456

可以看到,随着训练进行,训练损失和测试损失都在下降,说明模型正在学习。

3.5 模型评估与部署

训练完成后,我们需要全面评估模型性能:

# 模型评估
model.eval()
with torch.no_grad():
    test_predictions = model(X_test_tensor)
    
    # 计算各种评估指标
    from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
    
    # 均方误差
    mse = mean_squared_error(y_test, test_predictions.numpy())
    # 均方根误差(单位:万元)
    rmse = np.sqrt(mse)
    # 平均绝对误差
    mae = mean_absolute_error(y_test, test_predictions.numpy())
    # R²分数(越接近1越好)
    r2 = r2_score(y_test, test_predictions.numpy())
    
    print(f"模型评估结果:")
    print(f"  均方根误差 (RMSE): {rmse:.2f} 万元")
    print(f"  平均绝对误差 (MAE): {mae:.2f} 万元")
    print(f"  R² 分数: {r2:.4f}")

# 使用模型进行预测
def predict_price(area, bedrooms, bathrooms, age, distance_to_center):
    """
    预测房价的实际使用函数
    
    参数:
        area: 面积 (平方米)
        bedrooms: 卧室数
        bathrooms: 浴室数
        age: 房龄 (年)
        distance_to_center: 距市中心距离 (公里)
    
    返回:
        预测房价 (万元)
    """
    # 转换为模型期望的格式
    input_data = np.array([[area, bedrooms, bathrooms, age, distance_to_center]])
    # 标准化
    input_scaled = scaler.transform(input_data)
    # 转为张量
    input_tensor = torch.FloatTensor(input_scaled)
    
    # 预测
    model.eval()
    with torch.no_grad():
        prediction = model(input_tensor)
    
    return prediction.item()

# 实际使用示例
predicted = predict_price(100, 3, 2, 5, 3)
print(f"对于一套100平米、3卧2卫、5年新、距中心3公里的房子")
print(f"预测房价: {predicted:.2f} 万元")

四、常见算法一览:从原理到适用场景

4.1 线性回归与逻辑回归:最简单的模型

线性回归是机器学习中最基础的算法之一。它的核心思想是:输出是输入的线性组合

比如,对于房价预测,模型可能是这样的:

房价 = w 1 × 面积 + w 2 × 卧室数 + w 3 × 浴室数 + w 4 × 房龄 + w 5 × 距市中心 + b \text{房价} = w_1 \times \text{面积} + w_2 \times \text{卧室数} + w_3 \times \text{浴室数} + w_4 \times \text{房龄} + w_5 \times \text{距市中心} + b 房价=w1×面积+w2×卧室数+w3×浴室数+w4×房龄+w5×距市中心+b

其中 w 1 , w 2 , . . . , w 5 w_1, w_2, ..., w_5 w1,w2,...,w5 是学习到的权重, b b b 是偏置。

线性回归模型结构

输出

加权求和

学习到的权重

输入特征

预测房价: 300万

面积: 100㎡

w₁ = 2.5

w₂ = -5.0

w₃ = 8.0

w₄ = -1.2

w₅ = -3.5

b = 50

卧室: 3个

浴室: 2个

房龄: 5年

距中心: 3km

100×2.5 + 3×(-5) + 2×8 + 5×(-1.2) + 3×(-3.5) + 50

线性回归适用于输出是连续值的问题(房价、温度、销售额等)。

逻辑回归虽然名字里有"回归",但实际上是一个分类算法。它输出的是一个0到1之间的概率值,适用于二分类问题(比如判断邮件是否为垃圾邮件)。

4.2 K近邻(KNN):物以类聚

K近邻算法的思想非常直观:如果你周围的邻居大多属于某个类别,那你也大概率属于这个类别

K近邻分类示例 (K=5)

投票结果

K=5个最近邻居

待分类点 (绿色问号)

找5个最近邻居

找5个最近邻居

找5个最近邻居

找5个最近邻居

找5个最近邻居

投票

投票

投票

投票

投票

红色3票 vs 蓝色2票

?

红色

红色

蓝色

蓝色

蓝色

分类为: 红色

计算距离

欧氏距离

KNN的优点是简单直观、易于理解;缺点是预测速度慢(需要计算与所有样本的距离)、对异常值敏感。

4.3 决策树:自动生成if-else规则

决策树通过不断问问题来对数据进行分类。这些问题不是人工设计的,而是算法自动从数据中学习得到的。

贷款申请决策树

问题3: 有房产?
style=fill:#e1bee7,stroke:#7b1fa2

问题2: 信用分大于700
style=fill:#fff8e1,stroke:#ff8f00

问题1: 收入大于5000
style=fill:#e3f2fd,stroke:#1565c0

收入不足

高收入好信用

有房产担保

进一步审核

批准

继续问问题2

批准

继续问问题3

拒绝

拒绝

批准

评估其他因素

批准

人工审核

决策树的优点是易于解释(可以可视化)、能处理非线性关系;缺点是容易过拟合(通过剪枝和设置深度限制来缓解)。

4.4 集成学习:三个臭皮匠赛过诸葛亮

集成学习的核心思想是:组合多个模型的预测,得到一个更好的结果

最典型的例子是随机森林——它由多棵决策树组成,每棵树都是随机地从数据和特征中采样后训练的。预测时,所有树各自投票,最终结果由投票决定。

Random Forest: Multiple Trees Integration

Predicting New Data

Prediction Stage

Training Stage

Voting Mechanism

New House Data

Training Set 1 (Random Sampling)

Decision Tree 1

Training Set 2 (Random Sampling)

Decision Tree 2

Training Set 3 (Random Sampling)

Decision Tree 3

Tree1: Price Up

Tree2: Price Down

Tree3: Price Up

2 votes大于1 vote Final Prediction Price Up

随机森林之所以有效,是因为:

  1. 多样性:每棵树看到的数据和特征略有不同
  2. 减少过拟合:多棵树的预测取平均,噪声会被抵消
  3. 稳定性:不容易被个别异常值带偏

五、业务实践:机器学习项目的常见陷阱

5.1 数据质量决定一切

在真实业务场景中,数据往往存在各种问题。我见过太多项目失败,不是因为模型不够好,而是因为数据质量太差。

常见数据问题

  1. 缺失值:有些样本的某些特征是空的。可能的原因包括用户没填写、数据库同步问题、数据采集设备故障等。

  2. 异常值:有些数值明显不合理。比如年龄显示为200岁、工资显示为负数。这些可能是录入错误或数据污染。

  3. 数据泄露:训练数据中包含了不应该用于预测的信息。比如,用"是否购买"作为特征来预测"购买转化",但这个特征本身就是购买的结果——这会导致模型"偷看答案"。

常见数据陷阱

陷阱3: 数据泄露

陷阱2: 异常值

陷阱1: 缺失值

影响

影响

影响

影响

影响

影响

影响

影响

影响

用户没填年龄

系统没记录职业

部分数据丢失

年龄 = 200岁

收入 = -1000元

价格 = 99999999

用结果预测结果

包含未来信息

训练测试污染

模型偏差

预测失真

虚高的准确率

避坑指南

# 检查缺失值
print(df.isnull().sum())
print(df.isnull().mean() * 100)  # 缺失百分比

# 处理缺失值的方法
# 方法1: 删除有缺失值的行(如果缺失比例小)
df_clean = df.dropna()

# 方法2: 用均值/中位数填充数值型
df['年龄'].fillna(df['年龄'].median(), inplace=True)

# 方法3: 用众数填充分类型
df['职业'].fillna(df['职业'].mode()[0], inplace=True)

# 方法4: 用模型预测缺失值(更高级)
# from sklearn.impute import KNNImputer
# imputer = KNNImputer(n_neighbors=5)
# df_imputed = imputer.fit_transform(df)

# 检查异常值
print(df.describe())  # 查看统计信息
# 箱线图检测: 超过1.5倍四分位距的值视为异常

# 处理异常值
# 方法1: 用边界值替代
df['收入'] = df['收入'].clip(lower=0)  # 把负值变为0

# 方法2: 删除异常值
df = df[(df['年龄'] > 0) & (df['年龄'] < 120)]

5.2 特征工程:让数据更适合模型

特征工程是机器学习中最能体现"经验"的部分。它指的是对原始数据进行转换,让模型更容易从中学习到规律。

常见的特征工程方法

# 1. 特征缩放:让不同规模的特征具有可比性
from sklearn.preprocessing import MinMaxScaler, StandardScaler

# 标准化 (均值0, 标准差1) - 适用于大多数情况
scaler = StandardScaler()
df['面积_标准化'] = scaler.fit_transform(df[['面积']])

# 归一化 (0到1之间) - 适用于有明确边界的情况
minmax = MinMaxScaler()
df['面积_归一化'] = minmax.fit_transform(df[['面积']])

# 2. 类别特征编码
# 方法1: 标签编码 (简单但不建议用于无序类别)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['城市_编码'] = le.fit_transform(df['城市'])

# 方法2: 独热编码 (One-Hot Encoding) - 推荐用于无序类别
df = pd.get_dummies(df, columns=['城市'], prefix='城市')

# 3. 特征组合
# 创建新特征:比如把"长"和"宽"组合成"面积"
df['面积_计算'] = df['长'] * df['宽']

# 4. 时间特征提取
df['月份'] = pd.to_datetime(df['日期']).dt.month
df['星期'] = pd.to_datetime(df['日期']).dt.dayofweek
df['是否周末'] = df['星期'].isin([5, 6]).astype(int)

5.3 模型选择不是一次性的

很多初学者会问:"我应该用什么模型?"答案是:看情况

模型选择决策树

小数据小于1万

大数据大于10万

分类

回归

高实时小于100ms

可接受延迟

数据量多大?

任务类型?

实时性要求?

随机森林 / XGBoost

梯度提升树 / 线性回归

轻量模型 / 在线学习

需要高精度?

深度学习 / 集成学习

随机森林 / XGBoost

但更重要的是:不要只试一个模型。在真实项目中,我通常会:

  1. 先试简单的模型(线性回归、逻辑回归)作为baseline
  2. 再试复杂的模型(随机森林、XGBoost、神经网络)
  3. 比较它们的性能和速度
  4. 考虑模型可解释性(有时候简单的模型更容易向业务方解释)
  5. 最终选择能平衡性能、速度、可解释性的模型

六、总结:机器学习的全局视图

让我们用一张图来回顾今天学到的所有概念:

机器学习全局视图

部署应用

模型评估

模型训练

数据准备

循环迭代

需要改进

表现良好

模型保存

数据收集

数据清洗

特征工程

选择模型

定义损失函数

梯度下降优化

过拟合/欠拟合检测

指标计算

调参优化

API封装

线上预测

五句话核心总结

第一句:机器学习的核心思想是让计算机从数据中自动学习规律,而不是人工编写规则。这就像教小孩认识动物,不是告诉他每种动物的具体定义,而是给他看很多例子,让他自己总结规律。

第二句:监督学习、无监督学习和强化学习是机器学习的三种主要范式。监督学习像是有标准答案的练习题;无监督学习像是没有老师的情况下发现规律;强化学习像是在试错中学习骑自行车。

第三句:损失函数衡量模型预测和真实值之间的差距,梯度下降是让损失最小化的核心算法。梯度下降就像一个盲人下山,他只能感觉到脚下的坡度,然后朝着坡度最陡的方向迈步,最终到达山谷(最小损失点)。

第四句:过拟合和欠拟合是模型复杂度和泛化能力之间的权衡。过拟合像是把教材背下来了但不会做新题;欠拟合像是连教材都没看懂。好的模型需要在这两者之间找到平衡。

第五句:数据质量往往比模型选择更重要。再好的模型也救不了垃圾数据。真实项目中,80%的时间都花在数据处理和特征工程上。


下篇预告

在下一篇文章中,我们将深入探讨具体的机器学习算法,从最基础的KNN、线性回归,到更强大的决策树、随机森林、XGBoost。我会通过大量的图解和代码示例,让你彻底理解每个算法的原理,并学会在实际项目中选择合适的算法。

敬请期待:《KNN到随机森林:常用机器学习算法的直观理解与实战》


参考资料

  • 机器学习经典教材:《机器学习》- 周志华(西瓜书)
  • 在线课程:吴恩达《Machine Learning》- Coursera
  • PyTorch官方文档:https://pytorch.org/docs/
  • Scikit-learn官方文档:https://scikit-learn.org/stable/

如果你觉得这篇文章有帮助,欢迎分享给需要的朋友。有任何问题或建议,也可以在评论区留言交流。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐