最近一年都非常的忙碌,所以一直没来得及更新 blog。近期终于有了一些时间,想在巩固自己的 coding 基础的同时也顺便更新一下 blog,接下来会努力更新一下深度学习 (Deep Learning, DL) 的代码教程,比较适用于基本掌握了 Python3 语法的同学,数学推导和语法方面的讲解会比较少,大多数代码是直接给出来的(以注释或者 print 的方式进行了一些讲解),一方面是因为时间仓促,另外一方面是因为完全可以把代码复制下来,然后粘贴给生成式人工智能(比如 ChatGPT、Gemini、deepseek、豆包等等)来得到讲解,所以就没有写过多的讲解内容。代码内容主要是想呈现常用的函数和 DL 的常见框架。

本次的基于 PyTorch 的深度学习入门教程计划更新 9 章左右的内容 (大概一个学期的时间),大致包含基础的 NumPy、PyTorch 知识;深度学习的代码三步框架以及一些 CV (Computer Vision) 和 NLP (Natural Language Processing) 的基本模型框架来完成 Deep Learning 方面的入门。

代码参考于《Python 深度学习基于 PyTorch》(第2版,机械工业出版社)。

1.0 为什么需要学习 NumPy

NumPy(Numerical Python)是 Python 科学计算的核心库,提供了高效的多维数组对象 ndarray 和丰富的数学运算函数。与原生 Python 的 list 不同,NumPy 数组在底层使用连续内存存储、类型统一,能够在 C 语言级速度下完成大规模向量与矩阵计算。它并不是 Python3 内置的一部分,而是通过 C 扩展实现的第三方库,这让 Python 拥有了真正意义上的数值计算能力。

在数据处理和科学计算中,原生 Python 的循环性能较低,无法支撑成千上万次的数值运算。NumPy 通过“向量化计算”让你只写一次运算逻辑,而由底层 C 实现自动完成批量计算,性能可提升数十倍。与此同时,NumPy 的广播机制让不同形状的数组也能自动对齐参与运算,大大简化了代码。它既是数据分析(如 Pandas、Matplotlib)的基础,也为深度学习框架(如 PyTorch、TensorFlow)提供了底层的张量运算思想。

而深度学习模型的核心是对多维矩阵(Tensor)的高效计算, NumPy 的 ndarray 就是最早的多维数组结构。像 PyTorch 的 Tensor、TensorFlow 的 tf.Tensor 都继承了 NumPy 的设计理念:支持矩阵运算、自动广播、批量操作。因此,熟练掌握 NumPy 是理解深度学习底层机制(如前向传播、梯度计算、参数更新)的必要基础。

学习 NumPy 最重要的是掌握两个核心:

  • ndarray:多维数组对象,是所有计算的载体;
  • ufunc(通用函数):对数组执行逐元素运算的高效函数,例如 np.exp()np.sin()np.add() 等。

除此之外,还应理解数组的索引与切片、广播机制、聚合函数(如 summean)、线性代数模块(dotinv)等。这些能力构成了深度学习框架的数值计算基础。所以我们先从 NumPy 的学习开始。

NumPy 的安装可以参照:NumPy - 安装 NumPy (下面代码均在 NumPy 2.2.6 下完成)

1.1 生成 NumPy

import numpy as np

# <-----利用已有数据生成数组----->
# 列表转换成 numpy 数组
lst1 = [3.14, 2.17, 0, 1, 2]
nd1 = np.array(lst1)
print(f"{nd1}, type: {type(nd1)}, shape: {nd1.shape}, ndim: {nd1.ndim}, dtype: {nd1.dtype}")
# [3.14 2.17 0. 1. 2. ], type: <class 'numpy.ndarray'>, shape: (5,), ndim: 1, dtype: float64

# 将嵌套列表转换成 numpy 数组
lst2 = [[3.14, 2.17, 0, 1, 2], [1, 2, 3, 4, 5]]
nd2 = np.array(lst2)
print(f"{nd2}, type: {type(nd2)}, shape: {nd2.shape}, ndim: {nd2.ndim}, dtype: {nd2.dtype}")
# [[3.14 2.17 0. 1. 2. ]
# [1. 2. 3. 4. 5. ]], type: <class 'numpy.ndarray'>, shape: (2, 5), ndim: 2, dtype: float64


# <-----利用 random 模块生成随机数数组----->
print('生成形状 (4, 4), 值在 0-1 之间的随机数数组:')
print(np.random.random((4, 4)), end='\n\n') # (4, 4) 和 [4, 4] 都可以

# low 默认是 0 high 默认是 1
print('生成形状 (3, 3), 值在 [1, 50) 之间的随机整数数组:')
print(np.random.randint(low=1, high=50, size=(3, 3)), end='\n\n')

print('生成数组元素是均匀分布的随机数组:')
print(np.random.uniform(low=1, high=3, size=(3, 3)), end='\n\n')

print('生成满足正态分布的形状为 (3, 3) 的随机数数组:')
print(np.random.randn(3, 3))

print('指定随机数种子,第一次生成随机数')
np.random.seed(10)
print(np.random.randint(1, 5, (2, 2)))
# 想要生成相同的数组,必须再次设置相同的随机数种子
np.random.seed(10)
print('指定相同的随机数种子,第二次生成随机数')
print(np.random.randint(1, 5, (2, 2)))


# <-----生成特定形状的多维数组----->
# 生成全是 0 的 3 行 4 列的二维数组
nd5 = np.zeros((3, 4))
print(nd5)
# 生成和 nd5 形状一样的全是 0 的矩阵
print(np.zeros_like(nd5), end='\n')
# 生成全是 1 的 3 行 4 列的二维数组
nd6 = np.ones([3, 4]) # () 和 [] 都可以
print(nd6)
# 生成三阶的单位矩阵
nd7 = np.eye(3)
print(nd7)
# 生成三阶的对角矩阵
nd8 = np.diag((1, 2, 3)) # (1, 2, 3) 和 [1, 2, 3] 都可以
print(nd8)


# <-----利用 arrage、 linspace 函数生成数组----->
# arange 函数类似于内置的 range 函数, 格式为 arrange(start, stop, step, dtype=None), start 默认为0, step 可以为小数
print(np.arange(10)) # [0 1 2 3 4 5 6 7 8 9]
print(np.arange(0, 10)) # [0 1 2 3 4 5 6 7 8 9]
print(np.arange(1, 4, 0.5)) # [1. 1.5 2. 2.5 3. 3.5]
print(np.arange(9, -1, -1)) # [9 8 7 6 5 4 3 2 1 0]

# np.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)
# endpoint=True 表示包含 stop, False 表示不包含 stop
# num 表示生成的等差数列的数量
print(np.linspace(0, 1, 11)) # [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]

1.2 读取数据

import numpy as np

# <====== 1.2 读取数据 =====>
# <-----Numpy 对数据的切片操作----->
np.random.seed(2025)
nd11 = np.random.random([10]) # 生成 10 个数的一维数组
print(f'用来操作的数组: {nd11}')
print('获取指定位置的数据,比如获取第四个元素:')
print(nd11[3])
print('截取一段数据,比如截取第4个到第6个数据')
print(nd11[3: 6])
print('截取固定间隔的数据,比如从第3个数据开始,每隔2个数据取一个,直到最后')
print(nd11[2::2])
print('倒序截取数据:')
print(nd11[::-1])
print('找出比 0.5 大的数据')
print(nd11[nd11 > 0.5])
print('截取一个多维数组的某个区域内的数据')
nd12 = np.arange(25).reshape([5, 5])
print(f'用来操作的二维数组:\n{nd12}')
print(f'截取第2-4行,第2-4列:\n{nd12[1:4, 1:4]}')
print(f'截取多维数组中数值在某个值域内的数据:\n{nd12[(nd12 > 3) & (nd12 < 10)]}')
# 注意这里不能写成 (nd12 > 3 and nd12 < 10),会报错
# 因为 and 只能用于布尔值之间的运算,而 & 可以用于数组之间的运算, nd12 > 3 返回的是一个布尔数组,所以要用 &
print(f'截取多维数组中的指定行, 比如读取第2、3行:\n{nd12[[1, 2]]}')
print(f'也可以写作"nd12[1:3, :]",结果一样:\n{nd12[1:3, :]}')
print(f'截取多维数组中的指定列, 比如读取第2、3列:\n{nd12[:, 1:3]}')


# <-----使用 choice 函数随机抽取数据----->
a = np.arange(1, 25, dtype=float)
choice1 = np.random.choice(a, size=[5, 6]) # 从 a 中随机抽取 3 行 4 列的数据,允许重复抽取
choice2 = np.random.choice(a, size=[3, 4], replace=False) # 不允许重复抽取
# replace=False 时,size 的值不能大于 a 的长度,否则会报错
# 下式中参数 p 指定每个元素对应的抽取概率,默认为每个元素被抽取的概率相等
choice3 = np.random.choice(a, size=[3, 4], p=a / np.sum(a))
print(f'用来操作的数组: {a}')
print(f'随机抽取的数组(允许重复):\n{choice1}')
print(f'随机抽取的数组(不允许重复):\n{choice2}')
print(f'随机抽取的数组(指定概率):\n{choice3}')

1.3 NumPy 的算数运算

import numpy as np

# <====== 1.3 Numpy 的算术运算 =====>
# <----- 逐元素操作 ----->
# 逐元素操作(对应元素操作)是指对两个形状相同的数组进行加、减、乘、除等运算时,是对对应位置的元素进行运算,输出的结果数组的形状与输入的两个数组形状相同
# 格式: np.multiply(arr1, arr2) # 乘法
# 其中 arr1 和 arr2 的对应元素相乘遵循广播机制(在1.8中介绍)
A = np.array([[1, 2], [-1, 4]])
B = np.array([[2, 0], [3, 4]])
print(f'A 和 B 逐元乘法(哈达玛积):\n{A * B}')
print(f'也可以用multiply,结果一致:\n{np.multiply(A, B)}')
print(f'A 和 B 逐元加法:\n{A + B}')
print(f'Numpy 数组也可以和标量进行运算(其间会用到广播机制),比如 A * 2:\n{A * 2}')
print(f'也可以用 multiply,结果已知:\n{np.multiply(A, 2)}')
print(f'数组经过一些激活函数处理后,输出与输入形状一致')
# 定义一些激活函数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def relu(x):
return np.maximum(0, x)
def softmax(x):
return np.exp(x) / np.sum(np.exp(x))
print(f'输入参数 X 的形状: {A.shape}')
print(f'激活函数 sigmoid 输出形状:{sigmoid(A).shape}')
print(f'激活函数 relu 输出形状:{relu(A).shape}')
print(f'激活函数 softmax 输出形状:{softmax(A).shape}')

# <----- 点积运算 ----->
# 点积运算是线性代数中非常重要的运算,点积运算又称为内积运算,矩阵乘法等
# 格式: np.dot(arr1, arr2) # 点积运算
# 其中 arr1 和 arr2 的点积运算遵循矩阵乘法规则
print(f'A 和 B 的点积运算:\n{A @ B}') # @ 号表示点积运算
print(f'也可以用 dot,结果一致:\n{np.dot(A, B)}')

1.4 数组变形

import numpy as np

# <====== 1.4 Numpy 数组变形 =====>
# <----- 修改数组形状 ----->
# 常见的函数
# arr.reshape(newshape) # 修改数组形状,返回一个新的数组,原数组不变
# arr.resize(newshape) # 修改数组形状,原数组改变
# arr.T # 转置数组,返回一个视图,原数组不变
# arr.flatten() # 将多维数组展平为一维数组,返回一个新的数组,原数组不变
# arr.ravel() # 将多维数组展平为一维数组,返回一个视图,改动会影响原数组
# arr.squeeze() # 去掉数组中维度为1的维度,返回一个新的数组,原数组不变。对多维数组使用不会报错但是无效
# arr.transpose() # 转置数组,返回一个新的数组,原数组不变

# 1) reshape 函数
arr = np.arange(10)
print('*' * 10 + ' reshape 函数 ' + '*' * 10)
print(f'原数组: {arr}, shape: {arr.shape}')
print(f'将向量 arr 变换成 2 行 5 列后的数组: {arr.reshape([2, 5])}, shape: {arr.reshape([2, 5]).shape}')
print(f'指定-1 让 numpy 自动计算该维度的大小: {arr.reshape([5, -1])}, shape: {arr.reshape([5, -1]).shape}')
print(f'将列指定为 2,行数自动计算: {arr.reshape([-1, 2])}, shape: {arr.reshape([-1, 2]).shape}')
# 要注意的是所指定的行数或者列数一定要可以被整除,否则会报错,比如 reshape([3, -1]) 就会报错,因为 10 不能被 3 整除
print(f'原数组仍然不变: {arr}, shape: {arr.shape}')

# 2) resize 函数
print('*' * 10 + ' resize 函数 ' + '*' * 10)
arr = np.arange(10)
print(f'原数组: {arr}, shape: {arr.shape}')
arr.resize([2, 5])
print(f'将向量 arr 变换成 2 行 5 列后的数组(原数组改变): {arr}, shape: {arr.shape}')

# 3) T 函数
print('*' * 10 + ' T 函数 ' + '*' * 10)
arr = np.arange(12).reshape([3, 4])
print(f'原数组: \n{arr}, shape: {arr.shape}')
print(f'转置后的数组: \n{arr.T}, shape: {arr.T.shape}')

# 4) flatten 函数
print('*' * 10 + ' flatten 函数 ' + '*' * 10)
arr = np.arange(12).reshape([3, 4])
print(f'原数组: \n{arr}, shape: {arr.shape}')
print(f'展平后的数组: \n{arr.flatten()}, shape: {arr.flatten().shape}')
print(f'原数组仍然不变: \n{arr}, shape: {arr.shape}')

# 5) ravel 函数
print('*' * 10 + ' ravel 函数 ' + '*' * 10)
arr = np.arange(6).reshape([2, -1])
print(f'原数组: \n{arr}, shape: {arr.shape}')
# ravel 函数接受一个按 C 语言格式(行优先) 或者 Fortran 语言格式(列优先) 展平的参数,默认是按 C 语言格式展平
print(f'按列优先展平后的数组: \n{arr.ravel("F")}, shape: {arr.ravel("F").shape}') # 按列优先展平
print(f'按行优先展平后的数组: \n{arr.ravel()}, shape: {arr.ravel().shape}') # 按行优先展平
print(f'注意 flatten 返回的是一个新的数组,而 ravel 返回的是一个视图,改动会影响原数组,下面是一个示例:')
a = np.arange(6).reshape([2, 3])
print(f'演示的数组: \n{a}')
# flatten()
b = a.flatten()
b[0] = 999
print("a after flatten:", a) # 不变
# ravel()
c = a.ravel()
c[0] = 888
print("a after ravel:", a) # 改变

# 6) squeeze 函数
print('*' * 10 + ' squeeze 函数 ' + '*' * 10)
arr = np.arange(3).reshape([3, 1])
print(f'原数组: \n{arr}, shape: {arr.shape}')
print(f'去掉维度为1的维度后的数组: \n{arr.squeeze()}, shape: {arr.squeeze().shape}')
arr1 = np.arange(6).reshape([3, 1, 2, 1])
print(f'原数组: \n{arr1}, shape: {arr1.shape}')
print(f'去掉维度为1的维度后的数组: \n{arr1.squeeze()}, shape: {arr1.squeeze().shape}')
arr2 = np.arange(6).reshape([3, 2])
print(f'对多维数组使用 squeeze 不会报错但是无效: \n{arr2.squeeze()}, shape: {arr2.squeeze().shape}')

# 7) transpose 函数
print('*' * 10 + ' transpose 函数 ' + '*' * 10)
arr = np.arange(24).reshape(2, 3, 4)
print(f'原数组: \n{arr}, shape: {arr.shape}')
print(f'转置后的数组: \n{arr.transpose(1, 2, 0)}, shape: {arr.transpose(1, 2, 0).shape}') # [3, 4, 2] RGB -> GBR


# <----- 合并数组 ----->
# 1) append 函数
print('*' * 10 + ' append 函数 ' + '*' * 10)
# 合并一维数组
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = np.append(a, b) # 如果不指定 axis,它会先把数组展平再追加。
print(f'用 append 合并一维数组的结果: {c}, shape: {c.shape}')
# 合并多维数组
a = np.arange(4).reshape(2, 2)
b = np.arange(4).reshape(2, 2)
# 按行合并
c = np.append(a, b, axis=0)
print(f'用 append 按行合并二维数组的结果:\n{c}, shape: {c.shape}')
# 按列合并
d = np.append(a, b, axis=1)
print(f'用 append 按列合并二维数组的结果:\n{d}, shape: {d.shape}')
# 如果维度不对,np.append 会报错,比如 axis=1 时要保证行数一致。

# 2) concatenate 函数
# 沿指定轴 **连接** 数组或矩阵
# 必须保证所有数组在指定 axis 以外的维度一致。 concatenate 的性能比 append 好
# concatenate 支持多个数组的合并,而 append 只能合并两个数组
print('*' * 10 + ' concatenate 函数 ' + '*' * 10)
a = np.arange(1, 5).reshape(2, 2)
b = np.array([[5, 6]])
c = np.concatenate((a, b), axis=0)
print(f'用 concatenate 按行合并二维数组和一维数组的结果:\n{c}, shape: {c.shape}')
d = np.concatenate((a, b.T), axis=1)
print(f'用 concatenate 按列合并二维数组和一维数组的结果:\n{d}, shape: {d.shape}')

# 3) stack 函数
# 沿指定轴 **堆叠** 数组或矩阵
print('*' * 10 + 'stack 函数 ' + '*' * 10)
a = np.arange(1, 5).reshape(2, 2)
b = np.array([[5, 6], [7, 8]])
c = np.stack((a, b), axis=0) # 在第0轴堆叠(行堆叠)
print(f'用 stack 在第0轴堆叠二维数组的结果:\n{c}, shape: {c.shape}')
d = np.stack([a, b], axis=1) # 在第1轴堆叠(列堆叠)
print(f'用 stack 在第1轴堆叠二维数组的结果:\n{d}, shape: {d.shape}')

# 4) zip 函数
# zip 是 python 的内置函数
print('*' * 10 + 'zip 函数' + '*' * 10)
a = np.arange(1, 5).reshape(2, 2)
b = np.arange(5, 9).reshape(2, 2)
c = list(zip(a, b)) # zip 返回的是一个迭代器,需要用 list 转换成列表
print(f'用 zip 合并二维数组的结果:\n{c}, length: {len(c)}')
# 用 zip 函数组合两个向量
a = [1, 2, 3]
b = [4, 5, 6]
c = zip(a, b)
for i, j in c:
print(i, end=', ')
print(j)
# zip 还可以用来解压
a = [(1, 4), (2, 5), (3, 6)]
b, c = zip(*a)
print(f'用 zip 解压后的结果: {b}, {c}') # zip(*arr) 的作用相当于矩阵的转置

1.5 批处理

import numpy as np

# <====== 1.5 Numpy 批处理 =====>
# 批处理 (mini-batch) 是机器学习和深度学习中常用的一种技术,指的是将数据集划分为多个小批次,每个小批次包含一定数量的数据样本,然后在每个小批次上进行训练或预测。
# 这样做的好处是可以减少内存占用,提高计算效率,同时也可以增加模型的泛化能力,避免过拟合。
data = np.random.randn(10000, 2, 3) # 生成 10000 个 2 行 3 列的二维数组
print(f'第 1 个维度为样本数,后两个是数据形状: {data.shape}')
np.random.shuffle(data) # 打乱数据
batch_size = 128 # 定义批处理的大小
for i in range(0, data.shape[0], batch_size): # data.shape[0] = len(data)
batch_data = data[i:i + batch_size] # 获取当前批次的数据
# 在这里对 batch_data 进行训练或预测操作
x_batch_sum = np.sum(batch_data)
print(f'第 {i // batch_size + 1} 个批次,shape: {batch_data.shape},当前批次数据的和: {x_batch_sum}\n')

1.6 节省内存

import numpy as np

# <====== 1.6 节省内存 =====>
# <----- X = X + Y 和 X += Y 的区别 ----->
# X = X + Y 和 X += Y 的结果是一样的,但是内存使用上有区别,X = X + Y 会创建一个新的数组,然后将结果赋值给 X,而 X += Y 则是在原有的 X 数组上进行修改,不会创建新的数组。
Y = np.random.randn(10, 2, 3)
X = np.zeros_like(Y)
print(f'初始时 X 的内存地址: {id(X)}')
X = X + Y
print(f'执行 X = X + Y 后 X 的内存地址: {id(X)}') # 内存地址变了
X = np.zeros_like(Y) # 重新初始化 X
print(f'重新初始化后 X 的内存地址: {id(X)}')
X += Y
print(f'执行 X += Y 后 X 的内存地址: {id(X)}') # 内存地址没变

# <----- X = X + Y 和 X[:] = X + Y 的区别 ----->
# X[:] = X + Y 则是在原有的 X 数组上进行修改,不会创建新的数组
# X[:] = X + Y 和 X += Y 区别在于 X += Y 是原地操作,而 X[:] = X + Y 不是原地操作。
# 在不确定类型、需要广播、安全执行等情况下,推荐使用 X[:] = X + Y(调试阶段)
Y = np.random.randn(10, 2, 3)
X = np.zeros_like(Y)
print(f'初始时 X 的内存地址: {id(X)}')
X = X + Y
print(f'执行 X = X + Y 后 X 的内存地址: {id(X)}') # 内存地址变了
X = np.zeros_like(Y) # 重新初始化 X
print(f'重新初始化后 X 的内存地址: {id(X)}')
X[:] = X + Y
print(f'执行 X[:] = X + Y 后 X 的内存地址: {id(X)}') # 内存地址没变

1.7 通用函数

import numpy as np
import math
import time

# <====== 1.7 Numpy 通用函数 =====>
# Numpy 中常见的函数有 sqrt、sin、cos、abs、dot、log、log2、log10、exp、cumsum、cumproduct、sum、mean、median、std、var、corrcoef等
# np.max、np.sum、np.min 函数中都需要指定 axis 参数,表示沿着哪个轴进行操作,axis=0 表示沿着列方向操作,axis=1 表示沿着行方向操作
# 不指定 axis 参数时,表示对整个数组进行操作

# <----- math 与 Numpy 性能比较 ----->
# math 模块中的函数只能处理标量,而 Numpy 中的函数可以处理数组,因此 Numpy 的性能更高;并且 Numpy 的函数是用 C 语言实现的,速度更快
x = [i * 0.001 for i in range(1000000)] # 生成 100 万个数的列表
start = time.time()
for i, t in enumerate(x):
x[i] = math.sqrt(t) # 使用 math.sqrt 计算平方根
end = time.time()
print(f'math 模块计算平方根耗时: {end - start} 秒')
x = np.array(x) # 将列表转换为 numpy 数组
start = time.time()
x = np.sqrt(x) # 使用 numpy.sqrt 计算平方根
end = time.time()
print(f'numpy 模块计算平方根耗时: {end - start} 秒\n')

# <----- Numpy 中常见的函数 ----->
A = np.eye(3) # 生成三阶单位阵
B = np.arange(15).reshape(3, 5)
print("B 的总和:", np.sum(B))
print("B 每行和:", np.sum(B, axis=1))
print("B 每列和:", np.sum(B, axis=0))

print("B 的均值:", np.mean(B))
print("B 每行的均值", np.mean(B, axis=1))
print("B 的标准差:", np.std(B)) # 也可以指定 axis 参数
print("B 的最小值:", np.min(B)) # 也可以指定 axis 参数
print("B 的最大值:", np.max(B)) # 也可以指定 axis 参数
print("B 的最大值位置:", np.argmax(B)) # 也可以指定 axis 参数

print("A 和 B 的点积:\n", A.dot(B))

1.8 广播机制

import numpy as np

# <====== 1.8 广播机制 =====>
# Numpy 通常要求参与运算的数组形状相同,但是有时候我们希望对不同形状的数组进行运算,这时候就需要用到广播机制(Broadcasting)。
# 广播机制是一种强大的机制,可以让不同形状的数组进行运算,而不需要显式地复制数据,从而节省内存和计算时间。
# 比如之前我们对一个数组 * 2,其实就是对数组的每个元素都乘以 2,这时候 2 会被广播成一个和数组形状相同的数组,然后再进行逐元素操作。
# 广播机制的基本规则如下:
# 1. 如果两个数组的维度数(ndim)不同,那么维度较少的数组会在最左边补1,直到两个数组的维度数相同;
# 比如 a 为 2×3×2,b 为 3×2,则 b 向着 a 看齐,在最左边补 1 变成 1×3×2
# 2. 输出数组的形状是输入数组形状的各个轴上的最大值;
# 3. 如果两个数组在某个维度上的长度相同,或者其中一个数组在该维度上的长度为1,那么它们在该维度上是兼容的;
# 4. 如果两个数组在某个维度上的长度不同,且都不为1,那么它们在该维度上是不兼容的,无法进行广播;
# 5. 当输入数组的某个轴的长度为 1 时,沿着此轴运算时都用 (或复制) 此轴上的第一组值;
# 6. 广播之后,两个数组的形状会变成相同的形状,然后进行逐元素操作。

A = np.arange(0, 40, 10).reshape(4, 1)
B = np.arange(0, 3)
print(f'矩阵 A:\n{A}, A的形状:{A.shape},\n矩阵 B:\n{B}, B的形状{B.shape}')
C = A + B
print(f'矩阵 C 的形状:{C.shape}')
print(f'A + B 的运算结果:\n{C}')
# 运算结果解释:
# 根据规则 1,B 向 A 看齐,B 从 [3] 变成 [1, 3]
# 根据规则2,生成的矩阵形状应该是 [4, 3]
# 根据规则5,A 的 axis = 1 轴为 1,所以复制了 2 份 [[0] [10] [20] [30]] 变成:
# [[ 0] [ 0] [ 0]
# [10] [10] [10]
# [20] [20] [20]
# [30] [30] [30]]
# B 的 axis = 0 轴为 1,所以复制 3 份 [[0 1 2]] 变成:
# [[0 1 2]
# [0 1 2]
# [0 1 2]
# [0 1 2]]
# 然后两个 [4, 3] 矩阵进行相加变成:
# [[ 0 1 2]
# [10 11 12]
# [20 21 22]
# [30 31 32]]