03 NumPy 入门:机器学习中的数组和矩阵

前言

前两篇分别介绍了机器学习的基本流程和 Python 环境。接下来要认识机器学习代码里最常见的数据结构:数组和矩阵。

我们看到的原始数据形式差别很大。照片由像素组成,文本可以被转换为词频或向量,Excel 表格包含一行行记录。进入模型之前,它们通常都要变成数字,并按照一定形状排列成数组。模型训练时所做的大量工作,本质上是在这些数字上进行加法、乘法、求平均值和矩阵运算。

Python 自带的列表可以存放数字,但机器学习需要处理成千上万甚至更多的数据。NumPy 提供的数组更适合这类任务。本文不会把 NumPy 写成函数手册,而是围绕一个问题展开:如何把 100 名学生的三项特征组织成模型能够使用的矩阵,并完成一次简单计算。

为什么机器学习需要 NumPy

Python 原生 list 很灵活,同一个列表可以同时保存整数、字符串和其他对象。这种灵活性适合普通程序,却不是大规模数值计算的最佳选择。

首先,列表里的元素通常是独立的 Python 对象。对大量数字逐个计算时,解释器需要不断读取对象并执行循环,额外开销比较明显。其次,列表没有直接定义逐元素乘法、按列平均值或矩阵乘法等数值语义。二维列表也只是"列表中再放列表",行长度甚至可以不一致。

NumPy(Numerical Python,数值 Python)通过 ndarray 提供多维数组。数组中的元素通常使用统一的数据类型,并以更紧凑、规则的方式存储。许多数值运算在底层实现,避免我们在 Python 层手写大量循环。

例如,给 10000 个数同时加 1,NumPy 可以直接写成 arr + 1。这种一次描述整组计算的写法叫向量化计算(Vectorized Computation)。它不仅代码更短,也更接近"对整列特征进行处理"的机器学习思路。

不过,NumPy 不是让任何代码自动变快的魔法。数组类型、形状和内存布局仍会影响结果与性能。入门阶段最重要的是养成两个习惯:计算前先看 shape,计算后检查结果形状。

NumPy 是什么

NumPy 是 Python 科学计算生态中的基础库,核心对象是 ndarray,名称可以理解为"N 维数组"。这里的"维"不是数据有几个元素,而是需要几个索引才能定位一个元素。

  • 一维数组像一排数字,例如三名学生的分数;
  • 二维数组像一张表,有行和列;
  • 三维及更高维数组可以表示多层数据,例如多张彩色图片。

在机器学习中,二维特征数组最常见。通常每一行代表一个样本,每一列代表一个特征。如果收集 100 名学生的学习时间、练习次数和睡眠时间,就可以得到形状为 (100, 3) 的数组:100 是样本数量,3 是每个样本的特征数量。

许多机器学习库把这个特征矩阵记为大写 X,把希望预测的目标记为 y。看到错误信息里出现 (100, 3)(3, 1) 时,不必害怕,它们只是在说明数据有多少行、多少列。

创建第一个 NumPy 数组

通常先导入 NumPy,并使用约定俗成的别名 np

复制代码
import numpy as np

一维数组可以这样创建:

复制代码
scores = np.array([1, 2, 3])
print(scores)

二维数组需要在外层列表中放入多行数据:

复制代码
matrix = np.array([
    [1, 2],
    [3, 4],
])

np.array() 会根据输入推断数据类型和形状。上面的 scores 是一维数组,matrix 是两行两列的二维数组。可以用四个属性观察它们:

复制代码
print(matrix.shape)  # (2, 2)
print(matrix.ndim)   # 2
print(matrix.dtype)  # 具体结果取决于平台和输入
print(matrix.size)   # 4

这些属性不执行复杂计算,却是排查机器学习数据问题时最先应该看的信息。

数组的基本属性

shape:数组的形状

shape 返回一个元组,依次记录每个轴的长度。形状 (100, 5) 表示 100 行、5 列。在常见的特征矩阵中,它通常对应"100 个样本、每个样本 5 个特征"。

顺序不能交换。(100, 5)(5, 100) 包含相同数量的元素,但前者通常表示 100 个样本,后者更像把特征放到了行上。模型期待前一种形式时,传入后一种就会报错或产生错误含义。

ndim:数组维度

ndim 表示数组有几个轴。一维数组的 ndim 是 1,二维矩阵是 2。需要注意,np.array([1, 2, 3]) 的形状是 (3,),不是 (3, 1)。前者是一维数组,后者是三行一列的二维数组。

dtype:元素的数据类型

dtype 表示数组中元素的统一类型,例如 int64float64。机器学习计算经常使用浮点数。整数数组参与除法时通常会产生浮点结果,但把浮点结果写回整数数组可能截断小数或报错,因此修改数据前要留意类型。

size:元素总数

size 是所有轴长度的乘积。形状为 (100, 3) 的数组共有 300 个元素。它与 len(arr) 不同:len() 只返回第一轴长度,也就是这里的 100。

NumPy 中的索引和切片

索引用来获取某个位置的元素。对一维数组,arr[0] 表示第一个元素;Python 从 0 开始计数。

复制代码
arr = np.array([10, 20, 30])
print(arr[0])  # 10

二维数组可以同时写行索引和列索引:

复制代码
data = np.array([
    [2.0, 10],
    [3.5, 18],
    [5.0, 25],
])

print(data[0, 1])  # 第 1 行、第 2 列

切片用于选择一段数据。冒号 : 表示该轴上的全部元素:

复制代码
first_feature = data[:, 0]
first_two_students = data[:2, :]

data[:, 0] 选择所有行的第 1 列。在机器学习代码中常见的 X[:, 0] 也是这个意思:取出所有样本的第一项特征。X[:, 1:3] 则保留第 2 列到第 3 列,右边界 3 不包含在结果中。

切片通常返回原数组的视图,而不是完全独立的副本。修改切片可能同时改变原数组。需要独立数据时,可以显式调用 .copy()

常用数组操作

创建特殊数组

np.zeros() 可以创建全 0 数组,常用于初始化结果;np.ones() 创建全 1 数组;np.arange() 按给定步长生成连续数值。

复制代码
zeros = np.zeros((2, 3))
ones = np.ones((2, 3))
numbers = np.arange(0, 10, 2)

传给 zerosones(2, 3) 是目标形状。np.arange(0, 10, 2) 从 0 开始、到 10 之前结束,步长为 2,结果是 [0, 2, 4, 6, 8]

数学计算

NumPy 的算术运算通常逐元素进行:

复制代码
values = np.array([2.0, 4.0, 6.0])
print(values + 1)       # 每个元素加 1
print(values * 2)       # 每个元素乘 2
print(values.mean())    # 平均值
print(values.max())     # 最大值
print(values.min())     # 最小值

对二维数组使用 mean(axis=0),可以分别计算每一列的平均值;mean(axis=1) 则计算每一行的平均值。机器学习预处理经常需要按列统计,因为一列通常对应同一种特征。

这些写法并不是"永远禁止 for 循环"。当每个样本需要复杂条件逻辑时,循环仍然有价值。但能用清晰数组运算表达的批量数值计算,优先向量化通常更简洁,也能利用 NumPy 的底层实现。

怎样理解 axis

axis 是初学 NumPy 时很容易混淆的参数。可以把二维数组想成一张表:axis=0 表示沿着行的方向把多行压缩掉,最后每一列留下一个统计值;axis=1 表示沿着列的方向把多列压缩掉,最后每一行留下一个统计值。

复制代码
students = np.array([
    [2.0, 10, 7.0],
    [4.0, 20, 8.0],
])

column_means = students.mean(axis=0)
row_means = students.mean(axis=1)

column_means 的形状是 (3,),分别对应三项特征的平均值;row_means 的形状是 (2,),分别对应两个样本各自三列数字的平均值。后者虽然可以计算,但"学习小时、练习次数和睡眠小时"的单位不同,直接求行平均值通常没有实际意义。这也提醒我们:NumPy 只负责按照指令计算,不会判断统计方式是否符合业务含义。

如果暂时分不清轴,先用一个两行三列的小数组实验,再观察输入和输出的 shape。不要只背"0 是列、1 是行"这样的口诀,因为在三维数组中会更容易混乱。更可靠的理解是:指定哪个轴,就压缩哪个轴;没有被压缩的轴决定结果中还剩多少个数。

矩阵运算与机器学习

机器学习模型经常要把每个样本的多个特征按不同权重组合起来。假设特征矩阵 X 有 100 行、3 列,权重矩阵 W 有 3 行、1 列,那么:

复制代码
result = X @ W

运算符 @ 表示矩阵乘法。结果形状是 (100, 1):每个样本得到一个数。直观上,每一行的三个特征分别乘以三个权重,再把结果相加。

线性模型常写成:

复制代码
y = XW + b

其中 X 是输入特征,W 是权重,b 是偏置,y 是输出。这里不需要展开数学推导,先记住形状关系即可:X 的列数必须等于 W 的行数,否则矩阵乘法无法进行。

真实训练会根据数据不断调整权重,而不是手工指定。本文实践中的权重只用于演示 X @ W 如何把三列特征转换为一列结果,不能解释为真实的成绩规律。

第一个 NumPy 实践案例

我们用固定随机种子构造 100 个模拟学生样本,每个样本有三项特征:学习时间、练习次数和睡眠时间。代码先用 np.column_stack() 把三组一维数据按列合并为 X,再沿 axis=0 计算各列的平均值、最高值和最低值。

随后创建形状为 (3, 1) 的权重矩阵,执行 XW + b,得到 (100, 1) 的模拟预测结果。

完整可运行代码

将下面代码保存为 03_numpy_demo.py

复制代码
"""NumPy 入门示例:构造学生特征矩阵并完成简单矩阵计算。"""

from __future__ import annotations

import sys


def load_numpy():
    """导入 NumPy;缺少或损坏时给出清晰的错误信息。"""
    try:
        import numpy as np
    except ModuleNotFoundError as exc:
        raise RuntimeError(
            "当前 Python 环境缺少 NumPy,请先确认解释器和 Conda 环境。"
        ) from exc
    except Exception as exc:
        raise RuntimeError(f"NumPy 已被找到,但导入失败:{exc}") from exc
    return np


def main() -> None:
    """生成 100 个模拟样本,统计特征并执行 X @ W。"""
    if sys.version_info < (3, 8):
        raise RuntimeError("本示例需要 Python 3.8 或更高版本。")

    np = load_numpy()
    rng = np.random.default_rng(42)

    # 三列分别表示学习时间、练习次数和睡眠时间。
    study_hours = rng.uniform(1.0, 10.0, size=100).round(1)
    practice_counts = rng.integers(0, 51, size=100)
    sleep_hours = np.clip(
        rng.normal(loc=7.0, scale=0.8, size=100), 4.5, 9.5
    ).round(1)
    x = np.column_stack((study_hours, practice_counts, sleep_hours))

    if x.shape != (100, 3) or not np.isfinite(x).all():
        raise ValueError("特征矩阵必须是 100 行 3 列,并且不能包含无效数值。")

    feature_names = ("学习时间(小时)", "练习次数(次)", "睡眠时间(小时)")
    means = x.mean(axis=0)
    maximums = x.max(axis=0)
    minimums = x.min(axis=0)

    # 这里的权重只用于演示矩阵乘法,不是从真实成绩数据中训练得到的。
    weights = np.array([[5.0], [0.4], [1.5]], dtype=float)
    bias = 5.0
    predicted_scores = x @ weights + bias

    if predicted_scores.shape != (100, 1):
        raise ValueError("矩阵计算结果的形状不是预期的 (100, 1)。")

    print(f"NumPy: {np.__version__}")
    print(f"特征矩阵 shape: {x.shape}")
    print(f"特征矩阵 ndim: {x.ndim}")
    print(f"特征矩阵 dtype: {x.dtype}")
    print(f"特征矩阵 size: {x.size}")
    print("\n三列特征的统计结果:")
    for name, mean, maximum, minimum in zip(
        feature_names, means, maximums, minimums
    ):
        print(
            f"{name}: 平均值={mean:.2f}, 最高值={maximum:.2f}, "
            f"最低值={minimum:.2f}"
        )

    print(f"\n权重矩阵 shape: {weights.shape}")
    print(f"预测结果 shape: {predicted_scores.shape}")
    print(
        "前 5 个预测结果: "
        + np.array2string(predicted_scores[:5, 0], precision=2, separator=", ")
    )


if __name__ == "__main__":
    try:
        main()
    except (RuntimeError, ValueError) as exc:
        print(f"程序无法继续:{exc}", file=sys.stderr)
        sys.exit(1)

在项目根目录运行:

复制代码
python 03_numpy_demo.py

本示例已在 Conda base 环境中实际运行,使用 Python 3.11.4 和 NumPy 1.24.3,程序正常结束。固定种子 42 得到的输出如下:

复制代码
NumPy: 1.24.3
特征矩阵 shape: (100, 3)
特征矩阵 ndim: 2
特征矩阵 dtype: float64
特征矩阵 size: 300

三列特征的统计结果:
学习时间(小时): 平均值=5.38, 最高值=9.80, 最低值=1.10
练习次数(次): 平均值=23.97, 最高值=50.00, 最低值=0.00
睡眠时间(小时): 平均值=6.96, 最高值=9.00, 最低值=5.30

权重矩阵 shape: (3, 1)
预测结果 shape: (100, 1)
前 5 个预测结果: [64.05, 58.55, 68.85, 64.8 , 31.75]

三列数据合并后都变为 float64,因为同一个 NumPy 数组通常使用统一类型;练习次数虽然由整数生成,进入特征矩阵后也被转换成了浮点数。size 为 300,正好等于 100 个样本乘以 3 个特征。

NumPy 常见错误

1. ValueError: operands could not be broadcast together

广播(Broadcasting)是 NumPy 让不同形状数组参与逐元素运算的规则。例如形状 (100, 3) 的矩阵可以与形状 (3,) 的数组相加,因为后者能对应到三列。但 (100, 3)(2,) 无法按规则对齐,便会出现广播错误。解决时不要盲目 reshape,先打印双方 shape,明确希望按行还是按列计算。

2. 矩阵乘法的 shape 不匹配

A @ B 要求 A 的列数等于 B 的行数。(100, 3) @ (3, 1) 可以得到 (100, 1),而 (100, 3) @ (2, 1) 无法计算。错误信息里的维度数字通常已经指出冲突位置。

3. 整数和浮点类型问题

整数数组不能无损保存小数。如果标准化后的结果需要写回原整数数组,可能发生截断或类型转换错误。可以在创建时写 dtype=float,或使用 arr.astype(float) 得到浮点副本。转换前还要考虑内存占用和是否真的需要复制。

4. 一维数组和二维数组混淆

形状 (3,)(1, 3)(3, 1) 含义不同。机器学习模型往往要求二维输入,即使只有一个特征,也常写成 (样本数, 1)。可以用 reshape(-1, 1) 把一维特征转换为一列;-1 表示让 NumPy 根据元素总数自动推断行数。

NumPy 与后续机器学习库的关系

NumPy 负责底层数组和数值计算,是许多 Python 数据工具的共同基础。Pandas 在它之上提供带行列标签的表格结构,适合读取、清洗和分析数据;scikit-learn 提供数据预处理、模型训练和评估接口,常接收 NumPy 数组作为输入;PyTorch 使用"张量"进行深度学习计算,张量的形状、索引和广播思想与 NumPy 很接近,并增加了 GPU 与自动求导能力。

学习 NumPy 的目的不是记住所有函数,而是能读懂数据形状、选择需要的行列,并判断一次运算的输入与输出。掌握这些内容后,后续遇到 Pandas、scikit-learn 和 PyTorch 时,会发现许多概念是相通的。

总结

本文认识了 NumPy 的核心对象 ndarray,理解了 shapendimdtypesize,并练习了索引、切片、按轴统计和矩阵乘法。实践案例把 100 个样本、3 个特征组织成 (100, 3) 的矩阵,再通过 X @ W + b 得到一列模拟结果。

后续处理任何机器学习数据时,都可以先问三个问题:每一行代表什么,每一列代表什么,当前数组的 shape 是否符合计算要求。这比死记函数名称更有用,也是继续学习 Pandas、机器学习算法和深度学习的重要基础。

下一篇预告

下一篇是《Pandas 入门:机器学习中的数据读取与清洗》。我们会学习如何读取表格数据、处理缺失值、筛选行列,并把清洗后的数据交给后续模型。

相关推荐
bittersuite1 小时前
文本预处理,语言模型
人工智能·深度学习·机器学习
宇宙第一小趴菜1 小时前
七、机器学习建模流程
人工智能·机器学习
m0_519196401 小时前
【设计模式】java的习题
开发语言·python
pt10431 小时前
网络自动化Python课程:Git版本控制基础入门与实验演示
网络·python·自动化
circuitsosk1 小时前
不止于API调用:大模型推理加速与云原生服务化部署指南
python·云原生·agent·vllm·推理加速·大模型部署·ensorrt-llm
萌动的小火苗1 小时前
深度神经网络中,梯度消失和梯度爆炸的根本原因是什么?有哪些解决方法?【文末含面试万能总结】
人工智能·python·深度学习·神经网络·dnn
卷无止境2 小时前
当Python遇上并发:concurrent.futures的核心逻辑与实战技巧
后端·python
Ivanqhz2 小时前
泰勒展开(Taylor Expansion)
算法·决策树·机器学习·集成学习
卷无止境2 小时前
编程语言里到底有没有经济学规律?
后端·python