WRITING · 文章

2026 年 11 月 12 日|Python、NumPy 与 PyTorch 学习总结

ZRonin

学习内容: Python 基础语法、函数、数据结构、面向对象、异常处理、NumPy 数组计算、PyTorch Tensor。

学习目标: 从 C++ 过渡到 Python,掌握神经网络开发需要的编程基础,为后续 CNN 降噪模型训练做准备。

复习原则:

  • 保留自己编写的练习代码。
  • 记录实际出现的错误及原因。
  • 提供修改后的正确代码。
  • 详细整理每个已经讲过的知识点。
  • 通过 Python 与 C++ 的对照理解语法差异。

第一章:Python 基础语法

1.1 Python 与 C++ 的主要区别

Python 是一种高级、动态类型编程语言。

与 C++ 相比,Python 的语法更加简洁,许多操作由语言运行时自动完成。

代码对比

C++:

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    cout << a << endl;
    return 0;
}

Python:

a = 10
print(a)

两段程序均输出 10。

主要差异

特性C++Python变量类型静态类型动态类型变量声明int a = 10a = 10语句结束通常使用通常使用换行代码块{}缩进输出coutprint()函数定义写明返回类型使用def内存管理支持手动及自动管理主要自动管理性能通常更高普通 Python 循环通常较慢

Python 的简洁并不意味着底层不存在复杂操作,而是很多机制被运行时和标准库封装起来。

1.2 变量与数据类型

Python 定义变量时不需要指定类型:

a = 10
b = 3.14
name = "Eidovelis"
flag = True

常见数据类型:

类型名称示例int整数10float浮点数3.14str字符串"Hello"bool布尔值True、Falselist列表[1, 2, 3]tuple元组(1, 2, 3)dict字典{"a": 10}set集合{1, 2, 3}

type():获取数据类型

a = 10

print(type(a))  # <class 'int'>

a = "Hello"

print(type(a))  # <class 'str'>

Python 的变量名可以重新绑定到不同类型的对象。

例如:

a = 10
a = "Hello"

这在 Python 中是合法的。

但需要注意:动态类型不代表没有类型,只是不要求变量名始终绑定同一种类型。

1.3 Python 的运算符

算术运算符

运算符作用示例+加法3 + 2-减法3 - 2*乘法3 * 2/除法5 / 2 = 2.5//向下取整除法5 // 2 = 2%取模5 % 2 = 1**幂运算2 ** 3 = 8

需要特别注意:

print(5 / 2)   # 2.5
print(5 // 2)  # 2

Python 的 / 通常得到浮点数结果。

而 // 表示向负无穷方向取整,不是简单地去掉小数部分。例如 -5 // 2 得到 -3。

比较运算符

a == b  # 等于
a != b  # 不等于
a > b   # 大于
a < b   # 小于
a >= b  # 大于等于
a <= b  # 小于等于

特别区分:

a = 10   # 赋值

a == 10  # 判断是否相等

复合赋值

a += 1
a -= 1
a *= 2
a /= 2

例如:

total += num

等价于:

total = total + num

Python 没有 C++ 那样的 i++、i-- 运算符。


第二章:条件判断、循环与函数

2.1 if、elif、else

Python 的条件判断结构:

a = 20

if a > 30:
    print("大于30")

elif a > 10:
    print("大于10")

else:
    print("小于等于10")

C++ 与 Python 对照:

C++Pythonififelse ifelifelseelse

执行规则

if...elif...else 组成一个条件分支结构。

Python 从上往下判断:

  1. 如果 if 成立,执行其代码块。
  2. 否则检查下一个 elif。
  3. 如果前面条件都不成立,执行 else。
  4. 一个分支执行后,不会再执行同一结构中的其他分支。

两个独立 if 与 elif 的区别

if a > 10:
    print("A")

if a > 5:
    print("B")

如果 a=20,会同时输出 A 和 B。

而:

if a > 10:
    print("A")

elif a > 5:
    print("B")

只会输出 A。

因此,在最大值和最小值统计中,可以使用两个独立的 if。

2.2 for 循环

Python 的 for 循环可以直接遍历可迭代对象。

numbers = [10, 20, 30]

for num in numbers:
    print(num)

输出:

10
20
30

对应 C++ 范围 for:

for (int num : numbers)
{
    cout << num << endl;
}

Python 的 for 与 C++ 的范围 for 在使用上较为相似。

range() 的详细用法

语法:

range(start, stop, step)

参数含义:

  • start:起始值,默认 0。
  • stop:结束边界,不包含该值。
  • step:步长,默认 1。

例如:

for i in range(5):
    print(i)

依次输出:

0
1
2
3
4

再例如:

for i in range(1, 6):
    print(i)

输出 1、2、3、4、5。

指定步长:

for i in range(0, 10, 2):
    print(i)

输出:

0
2
4
6
8

反向遍历:

for i in range(5, 0, -1):
    print(i)

输出:

5
4
3
2
1

注意:range() 返回的是 range 对象,不是直接创建包含所有整数的列表。

如果想转换成列表:

a = list(range(1, 6))

print(a)

结果:

[1, 2, 3, 4, 5]

2.3 while 循环

Python:

i = 0

while i < 5:
    print(i)
    i += 1

对应 C++:

int i = 0;

while (i < 5)
{
    cout << i << endl;
    i++;
}

两者的逻辑相同:

  1. 判断条件。
  2. 条件成立,执行循环体。
  3. 再次判断条件。
  4. 条件不成立,结束循环。

Python 同样支持:

break     # 立即结束循环
continue  # 跳过本次循环的剩余语句

2.4 Python 函数

函数定义语法:

def function_name(parameters):
    # 函数体
    return result

例如:

def add(a, b):
    return a + b

result = add(10, 20)
print(result)

输出 30。

def 的作用

def 用来定义函数。

参数

def add(a, b):

a 和 b 是形参。

调用时:

add(10, 20)

10 和 20 是传入的实参。

return

return a + b

负责结束当前函数调用,并将计算结果返回给调用方。

需要区分:

print(a + b)

只是输出结果。

而:

return a + b

会将结果返回,以便后续保存或参与计算。

如果函数没有显式返回值,通常返回 None。

2.5 练习 01:偶数求和

题目: 定义函数,计算 1 到指定整数之间所有偶数的和。

我的原始代码

def evennumber_sum():
    total = 0

    for i in (1, n+1):
        total = total + 2

    return total

result = evennumber_sum(5)
print(result)

错误一:函数参数缺失

定义:

def evennumber_sum():

表示没有声明接收参数。

但调用:

evennumber_sum(5)

传入了一个参数。

应修改为:

def evennumber_sum(n):

错误二:误用元组

for i in (1, n+1):

这里的 (1, n+1) 是元组,并不是连续整数范围。

若 n=5,它只包含两个值:1 和 6。

应该使用:

for i in range(1, n+1):

错误三:累加逻辑错误

total = total + 2

只会在每次循环时让 total 增加 2。

但题目要求累加符合条件的整数本身。

修改后的代码

def evennumber_sum(n):
    total = 0

    for i in range(1, n + 1):
        if i % 2 == 0:
            total += i

    return total


result = evennumber_sum(100)
print(result)

输出:

2550

相关练习:奇数判断

奇数除以 2 的余数为 1:

if i % 2 == 1:

因此,只需要修改条件就可以把偶数求和程序改为奇数求和程序。

本章复习重点

重点理解以下内容:

  • Python 不使用 {} 划分代码块。
  • : 与缩进决定代码结构。
  • range() 的结束边界不包含在范围内。
  • for 可以遍历列表等可迭代对象。
  • return 和 print() 的区别。
  • 函数参数必须与调用方式对应。
  • if 和 elif 的执行逻辑不同。

第三章:Python 列表与列表推导式

3.1 list 列表

列表是一种有序、可修改的数据容器。

numbers = [10, 20, 30, 40]

与 C++ 的 std::vector 在使用方式上有相似之处。

索引访问

print(numbers[0])  # 10
print(numbers[1])  # 20

索引从 0 开始。

负数索引

Python 允许使用负数索引:

numbers[-1]  # 最后一个元素
numbers[-2]  # 倒数第二个元素

例如:

numbers = [10, 20, 30, 40]

print(numbers[-1])

输出 40。

负数索引是 Python 列表非常常用的特性。

3.2 append() 与 len()

append()

在列表末尾添加一个元素。

numbers = [10, 20, 30]

numbers.append(40)

print(numbers)

输出:

[10, 20, 30, 40]

类似 C++:

numbers.push_back(40);

len()

获取列表元素数量:

len(numbers)

相当于 C++ 中的:

numbers.size();

例如:

numbers = [10, 20, 30]

print(len(numbers))

输出 3。

3.3 Python 的对象引用

这是学习时专门讨论过的一个概念。

Python 没有普通语法层面的 C++ 式指针声明。

C++:

int a = 10;
int* p = &a;

cout << *p;

Python 不能写:

a = 10
p = &a

但 Python 存在对象引用机制。

例如:

a = [10, 20, 30]

b = a

b[0] = 100

print(a)

结果:

[100, 20, 30]

原因是 a 和 b 引用同一个列表对象。

这里并不是复制了一份独立列表。

可变对象与不可变对象

列表是可变对象:

a = [10, 20, 30]
b = a

b[0] = 100

会改变双方引用的同一个列表。

整数是不可变对象:

a = 10
b = a

b = 20

print(a)

输出仍然是 10。

因为 b = 20 是重新绑定对象,并没有修改整数 10。

总结

Python 变量名通常绑定到对象。

C++ 则可以直接操作变量存储、指针与内存地址。

Python 中遍历列表通常不需要手动移动指针。

3.4 练习 02:寻找列表最大值

我的原始代码

numbers = [12, 45, 7, 89, 23, 56]

Maxvalue = 0

for num in numbers:
    if Maxvalue < num:
        Maxvalue = num

print(Maxvalue)

对于本题的正数数据,可以正确输出 89。

潜在错误

Maxvalue = 0

如果所有数字都是负数,例如:

numbers = [-12, -45, -7, -89]

程序会错误地输出 0。

修改后的代码

numbers = [12, 45, 7, 89, 23, 56]

max_value = numbers[0]

for num in numbers:
    if num > max_value:
        max_value = num

print(max_value)

本题核心

用列表第一个元素作为初始最大值,可以正确处理普通的非空数值列表,包括全负数情况。

3.5 练习 03:最大值、最小值和平均值

我的原始代码

numbers = [15, 28, 7, 42, 19, 36, 11]

Max_value = numbers[0]
Mini_value = numbers[0]
average = 0
sum = 0

for num in numbers,:
    sum += num

    if Max_value < num:
        Max_value = num

    if Mini_value > num:
        Mini_value = num

average = sum / len(numbers)

print(Max_value)
print(Mini_value)
print(average)

错误一:for 后面多了逗号

for num in numbers,:

numbers, 在这里形成一个只包含列表的元组。

因此循环的 num 取得的是整个列表,而不是列表中的某个数字。

正确写法:

for num in numbers:

错误二:变量名与内置函数重名

sum = 0

Python 自带 sum() 函数。

虽然把变量命名为 sum 不一定立即报错,但会遮蔽当前作用域中的内置函数。

建议改成:

total = 0

修改后的代码

numbers = [15, 28, 7, 42, 19, 36, 11]

max_value = numbers[0]
min_value = numbers[0]
total = 0

for num in numbers:
    total += num

    if num > max_value:
        max_value = num

    if num < min_value:
        min_value = num

average = total / len(numbers)

print(max_value)
print(min_value)
print(average)

此程序只需要遍历列表一次。

时间复杂度

对于包含 n 个元素的列表,循环执行 n 次。

时间复杂度:

O(n)

额外只需要几个变量,因此额外空间复杂度:

O(1)

3.6 列表切片 Slicing

切片可以一次取得列表中的多个元素。

基本语法:

numbers[start:stop:step]

三个参数

start:

起始索引,包含这个位置。

stop:

结束边界,不包含这个位置。

step:

索引变化步长。

例如:

numbers = [10, 20, 30, 40, 50]

print(numbers[1:4])

输出:

[20, 30, 40]

因为取索引 1、2、3,不取索引 4。

常用切片

numbers[:3]    # 前三个元素

numbers[2:]    # 索引 2 到末尾

numbers[-3:]   # 最后三个元素

numbers[::2]   # 每隔一个元素取一个

numbers[::-1]  # 反转列表

为什么 [::-1] 能反转?

切片省略 start、stop,步长设为 -1。

Python 会按照负步长的默认边界,从最后一个元素开始向前访问。

因此:

numbers[::-1]

返回一个逆序的新列表。

3.7 列表推导式

列表推导式用于用简洁的表达式构建新列表。

例如:

numbers = [1, 2, 3, 4, 5, 6]

even = [num for num in numbers if num % 2 == 0]

结果:

[2, 4, 6]

完整结构:

[表达式 for 变量 in 可迭代对象 if 条件]

其执行逻辑为:

  1. 遍历可迭代对象。
  2. 检查筛选条件。
  3. 条件成立时计算前面的表达式。
  4. 将结果加入新列表。

等价的普通写法:

even = []

for num in numbers:
    if num % 2 == 0:
        even.append(num)

只有计算,没有筛选

squares = [i * i for i in range(1, 6)]

结果:

[1, 4, 9, 16, 25]

3.8 练习 04:切片与列表推导式

我的原始代码

numbers = [3, 8, 12, 5, 19, 24, 7, 16, 11, 30]

print(numbers[:6:-1])
print(numbers[2:7])

even = [num for num in numbers if num % 2 == 0]
print(even)

greater_ten = [i * 2 for i in numbers if i > 10]
print(greater_ten)

第一题纠错

numbers[:6:-1]

结果:

[30, 11, 16]

它是反向取得最后三个元素。

如果要求保持原来顺序:

numbers[-3:]

结果:

[16, 11, 30]

其他三题正确。

本章复习重点

  • 正数与负数索引。
  • append() 与 len()。
  • 列表的可变性与对象引用。
  • 列表切片三个参数的意义。
  • 列表推导式中表达式与条件的位置。
  • Python 列表与 C++ vector 的异同。

第四章:tuple、dict、set 与字符串格式化

4.1 tuple 元组

创建元组:

position = (10, 20, 30)

访问元素:

print(position[0])

输出 10。

元组与列表的主要区别是:元组不能直接改变已有元素的绑定位置。

例如:

position[0] = 100

会抛出 TypeError。

元组适合保存固定的一组值,例如坐标、配置项和多个返回结果。

需要注意,如果元组内部包含可变对象,那个对象内部的内容仍然可能变化。

4.2 dict 字典

字典保存键值对:

gpu = {
    "name": "RTX 4090",
    "memory": 24,
    "temperature": 65
}

它可以理解为按照键查找值的数据容器。

与 C++ 的 std::unordered_map 在使用思想上有相似之处,但实现与具体语义并不完全相同。

访问

print(gpu["name"])

输出:

RTX 4090

修改

gpu["memory"] = 32

修改指定键对应的值。

增加新键值对

gpu["power"] = 450

items()

for key, value in gpu.items():
    print(key, value)

items() 提供字典中的键值对。

keys() 与 values()

gpu.keys()

获取键的视图。

gpu.values()

获取值的视图。

gpu.items()

获取键值对的视图。

这些对象可以用于遍历。

4.3 set 集合

创建集合:

numbers = {1, 2, 2, 3, 3, 4}

重复的元素不会保留。

集合内容相当于:

{1, 2, 3, 4}

但集合的输出顺序不能依赖。

用途

集合适合:

  • 数据去重。
  • 快速判断某个元素是否存在。
  • 求交集、并集和差集。

例如:

a = {1, 2, 3}
b = {3, 4, 5}

print(a & b)  # 交集
print(a | b)  # 并集

集合不能像列表那样使用数字索引访问。

4.4 print() 的 end 参数

Python 的 print() 默认在末尾添加换行符。

例如:

print("Hello")
print("World")

输出:

Hello
World

如果不希望换行:

print("Hello", end="")
print("World")

输出:

HelloWorld

end 用于指定输出结束时追加的内容。

4.5 f-string

Python 可以使用格式化字符串:

name = "RTX 4090"
memory = 24

print(f"{name}: {memory} GB")

输出:

RTX 4090: 24 GB

其中:

  • 字符串前面的 f 表示格式化字符串。
  • {} 内可以放变量或表达式。
  • Python 会计算表达式,并将结果放进字符串。

例如:

print(f"2 + 3 = {2 + 3}")

输出:

2 + 3 = 5

4.6 练习 05:GPU 字典

我的原始代码

gpu = {
    "RTX 4090": 24,
    "RTX 4080": 16,
    "RTX 4070": 12,
    "RTX 4060": 8
}

print(gpu["RTX 4090"], end="")
print("GB")

for name, memory in gpu.items():
    print(name, memory, end="")
    print("GB")

for name, memory in gpu.items():
    if memory >= 16:
        print(name, memory, end="")
        print("GB")

代码逻辑正确。

优化后的代码

gpu = {
    "RTX 4090": 24,
    "RTX 4080": 16,
    "RTX 4070": 12,
    "RTX 4060": 8
}

print(f"RTX 4090: {gpu['RTX 4090']} GB")

for name, memory in gpu.items():
    print(f"{name}: {memory} GB")

for name, memory in gpu.items():
    if memory >= 16:
        print(f"{name}: {memory} GB")

本题知识点

gpu["RTX 4090"]

根据键查找值。

gpu.items()

遍历所有键值对。

for name, memory in gpu.items():

同时获取当前键与对应的值。

if memory >= 16:

按照显存容量筛选。

本章复习重点

  • 四种常见容器的区别。
  • 字典的键值对访问方式。
  • items()、keys()、values() 的含义。
  • print(end="") 的作用。
  • f-string 的语法和使用。

第五章:NumPy 与 ndarray 多维数组

5.1 NumPy 是什么?

NumPy 是 Python 科学计算的重要基础库。

导入方式:

import numpy as np

其中:

  • numpy 是模块名称。
  • np 是我们为它指定的别名。
  • as 用于设置导入别名。

之后:

np.array()

就是通过 np 访问 NumPy 中的 array。

5.2 ndarray 与 Python list 的区别

Python 列表:

a = [1, 2, 3]
b = [4, 5, 6]

print(a + b)

输出:

[1, 2, 3, 4, 5, 6]

这里的 + 是拼接。

NumPy:

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

print(a + b)

输出:

[5 7 9]

这里的 + 是逐元素加法。

为什么 NumPy 适合数值计算?

NumPy 的 ndarray 通常保存同一种数据类型的元素,并利用底层优化的数值计算实现执行数组操作。

与大量 Python 层的逐元素循环相比,这种方式通常更加高效。

这叫作向量化运算。

5.3 ndim:数组维度数量

例如:

a = np.array([1, 2, 3])

一维数组:

a.ndim  # 1

二维数组:

b = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

print(b.ndim)  # 2

三维数组:

c = np.zeros((3, 2, 2))

print(c.ndim)  # 3

注意,数组的维度是指索引轴数量,不一定代表真实空间维度。

5.4 shape:数组形状

shape 表示每个维度的长度。

一维:

a = np.array([1, 2, 3])

print(a.shape)

结果:

(3,)

二维:

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

print(a.shape)

结果:

(2, 3)

表示两行三列。

三维:

a = np.zeros((3, 2, 2))

形状:

(3, 2, 2)

三个轴长度分别为 3、2、2。

5.5 dtype:元素数据类型

使用:

print(a.dtype)

可以查看数组中元素的数值类型。

常见 NumPy 类型:

np.int32
np.int64
np.float32
np.float64

例如:

a = np.array([1, 2, 3], dtype=np.float32)

创建一个元素类型为 float32 的数组。

注意:不同操作系统和 NumPy 版本中,默认整数类型可能存在差异,因此需要时应该明确指定 dtype。

5.6 多维索引

例如:

a = np.array([
    [10, 20, 30],
    [40, 50, 60]
])

访问第二行第三列:

print(a[1, 2])

输出:

60

Python 索引从 0 开始。

NumPy 推荐使用:

a[1, 2]

而普通嵌套列表通常使用:

a[1][2]

5.7 练习 06:ndarray 基础

我的代码

import numpy as np

a = np.array([
    [3, 7, 2],
    [9, 5, 8],
    [4, 6, 1]
])

print(a.ndim)
print(a.shape)
print(a[1, 2])
print(a * 2)

运行结果

2
(3, 3)
8

[[ 6 14  4]
 [18 10 16]
 [ 8 12  2]]

全部正确。

本题总结

已经掌握:

  • 使用 np.array() 创建数组。
  • 通过 .ndim 查看维度。
  • 通过 .shape 查看形状。
  • 使用二维索引访问元素。
  • 使用 * 实现逐元素运算。

第六章:NumPy 广播机制 Broadcasting

6.1 广播是什么?

广播允许形状不同、但满足兼容规则的数组进行逐元素运算。

例如:

a = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

b = np.array([10, 20, 30])

print(a + b)

结果:

[[11 22 33]
 [14 25 36]
 [17 28 39]]

可以理解为:

第一行:

[1, 2, 3] + [10, 20, 30]

第二行:

[4, 5, 6] + [10, 20, 30]

第三行:

[7, 8, 9] + [10, 20, 30]

每一行都与 b 对应位置相加。

6.2 广播规则

从 shape 的最右侧维度开始逐一比较。

两个对应维度能够广播,需要满足:

  1. 两个维度相同。
  2. 或者其中一个维度长度为 1。

如果维度数量不同,可以将较短 shape 的左侧视为补了 1。

例如:

A: (3, 3)
B:    (3,)

可以理解为:

A: (3, 3)
B: (1, 3)

两个维度逐一比较:

  • 最右侧:3 和 3,相同。
  • 左侧:3 和 1,可以广播。

结果:

(3, 3)

不能广播的情况

例如:

A: (3, 2)
B:    (3,)

右侧对应维度为 2 和 3。

既不相同,也没有一个为 1,因此无法广播。

会触发 ValueError。

6.3 行向量与列向量的广播

这一点比较容易混淆。

假设:

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

a.shape = (2, 3)。

每一行加上同一个向量

b = np.array([10, 20, 30])

b.shape = (3,)。

print(a + b)

结果:

[[11 22 33]
 [14 25 36]]

每一行分别加上不同的值

b = np.array([
    [10],
    [20]
])

b.shape = (2, 1)。

print(a + b)

结果:

[[11 12 13]
 [24 25 26]]

可以理解为:

  • 第一行全部加 10。
  • 第二行全部加 20。

6.4 广播不是复制数组

在概念上,可以把广播理解为将某个维度扩展。

但实际计算时,NumPy 通常不需要完整复制出一个扩展后的数组。

这是它进行高效数值计算的重要特性之一。

6.5 练习 07:广播运算

我的原始代码

import numpy as np

a = np.array([
    [3, 7, 2],
    [9, 5, 8],
    [4, 6, 1]
])

b = np.array([
    [1, 2, 3]
])

c = a + b

print(c)
print(a.shape)

运行结果

[[ 4  9  5]
 [10  7 11]
 [ 5  8  4]]

(3, 3)

计算正确。

第一处需要注意的地方

b = np.array([[1, 2, 3]])

这里的 shape 是:

(1, 3)

而不是:

(3,)

两者维度数量不同。

第二处需要修改的地方

原代码:

print(a.shape)

题目要求输出结果 c 的形状。

正确写法:

print(c.shape)

这次两者结果相同,但习惯上应该验证真正的运算结果。

本章复习重点

  • 广播从最右侧维度比较。
  • 长度相等或其中一个长度为 1 才兼容。
  • (3,) 与 (1,3) 不是同一种 shape。
  • 广播是逐元素运算中的形状兼容机制。
  • 广播通常不需要真的复制大量数据。

第七章:reshape、矩阵乘法与 Axis

7.1 reshape() 改变数组形状

a = np.array([1, 2, 3, 4, 5, 6])

b = a.reshape(2, 3)

结果:

[[1 2 3]
 [4 5 6]]

原来:

shape = (6,)

现在:

shape = (2, 3)

reshape 的要求

改变形状时,元素总数量必须保持不变。

例如有 6 个元素:

a.reshape(2, 3)  # 可以
a.reshape(3, 2)  # 可以
a.reshape(6, 1)  # 可以
a.reshape(3, 3)  # 不可以

因为:

2 × 3 = 6
3 × 2 = 6
6 × 1 = 6
3 × 3 = 9

最后一种需要 9 个元素,原数组只有 6 个。

注意:reshape 可能返回共享底层数据的视图,也可能需要复制,具体取决于内存布局。

7.2 逐元素乘法与矩阵乘法

NumPy 使用 * 表示逐元素乘法。

例如:

a = np.array([
    [1, 2],
    [3, 4]
])

b = np.array([
    [5, 6],
    [7, 8]
])

print(a * b)

结果:

[[ 5 12]
 [21 32]]

对应位置相乘:

1×5=5
2×6=12
3×7=21
4×8=32

矩阵乘法 @

print(a @ b)

结果:

[[19 22]
 [43 50]]

计算规则是左矩阵的行与右矩阵的列做点积。

例如:

19 = 1×5 + 2×7

22 = 1×6 + 2×8

43 = 3×5 + 4×7

50 = 3×6 + 4×8

维度规则

假设:

A 的 shape 为:

(m, n)

B 的 shape 为:

(n, p)

那么 A @ B 的 shape 为:

(m, p)

也就是:

(m,n) @ (n,p) → (m,p)

内侧维度必须一致。

7.3 练习 08:reshape 与矩阵乘法

我的代码

import numpy as np

a = np.array([1, 2, 3, 4, 5, 6])

re = a.reshape(2, 3)

b = np.array([
    [3, 2],
    [1, 2],
    [9, 5]
])

c = re @ b

print(re)
print(c)
print(c.shape)

运行结果

[[1 2 3]
 [4 5 6]]

[[32 21]
 [71 48]]

(2, 2)

本题完全正确。

计算形状:

(2, 3) @ (3, 2) = (2, 2)

7.4 Axis:轴

这是今天重点讨论、也比较容易产生误解的知识点。

假设:

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

shape = (2, 3)。

NumPy 为每个维度编号。

axis=0

第一个轴。

对于二维数组,可以理解为沿着行索引变化的方向,即上下方向。

执行:

np.sum(a, axis=0)

得到:

[5 7 9]

具体计算:

1 + 4 = 5
2 + 5 = 7
3 + 6 = 9

也就是固定列索引,沿不同的行进行计算。

axis=1

第二个轴。

对应列索引变化的方向,即左右方向。

执行:

np.sum(a, axis=1)

得到:

[ 6 15]

具体计算:

1 + 2 + 3 = 6

4 + 5 + 6 = 15

也就是固定行索引,沿不同的列进行计算。

为什么 axis=0 得到每一列的结果?

因为 axis 指定的是要进行归约的轴。

在 sum、mean 等归约操作中,指定的轴默认会被消除。

例如:

原 shape: (2, 3)

axis=0 → (3,)

axis=1 → (2,)

最重要的理解方法:不是看最后结果有几行或几列,而是看哪个维度被消除了。

7.5 一维数组与二维数组

今天重点纠正过这个概念。

一维数组

a = np.array([1, 2, 3])

shape:

(3,)

ndim = 1。

它只有一个轴,长度为 3。

一行三列的二维数组

a = np.array([[1, 2, 3]])

shape:

(1, 3)

ndim = 2。

三行一列的二维数组

a = np.array([
    [1],
    [2],
    [3]
])

shape:

(3, 1)

ndim = 2。

重点

不能仅根据数字在屏幕上的横向或纵向排列,判断数组维度。

应该通过数组结构、ndim 和 shape 判断。

7.6 keepdims

keepdims 用于控制归约之后是否保留被计算的轴。

默认值:

keepdims=False

例如:

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

执行:

np.sum(a, axis=1)

结果:

[ 6 15]

shape:

(2,)

而:

np.sum(a, axis=1, keepdims=True)

结果:

[[ 6]
 [15]]

shape:

(2, 1)

原因:

  • False:删除归约轴。
  • True:保留归约轴,但长度变成 1。

为什么 keepdims 有用?

保留维度能够让后续的广播更加方便。

例如:

a = np.array([
    [1., 2., 3.],
    [4., 5., 6.]
])

row_mean = np.mean(a, axis=1, keepdims=True)

result = a - row_mean

此时:

a.shape        = (2, 3)
row_mean.shape = (2, 1)

通过广播,能够从每一行的三个元素中减去该行的平均值。

这个操作以后在神经网络数据处理中很常见。

7.7 NumPy 常用数学函数

Python 内置函数

sum(numbers)
min(numbers)
max(numbers)
len(numbers)

这些不需要导入 NumPy。

NumPy 函数

np.sum(a)
np.mean(a)
np.max(a)
np.min(a)

其中 np.mean() 是 NumPy 提供的函数,并不是 Python 内置函数。

Python 标准库的 statistics 模块也有平均值函数:

from statistics import mean

常用 NumPy 数学函数表

函数作用np.sum(a)求和np.mean(a)平均值np.max(a)最大值np.min(a)最小值np.std(a)标准差np.var(a)方差np.sqrt(a)平方根np.abs(a)绝对值np.exp(a)指数函数np.log(a)自然对数np.sin(a)正弦函数np.cos(a)余弦函数

这些函数中的许多都支持使用 axis 指定计算轴。

对象方法写法

例如:

np.mean(a)

也可以写成:

a.mean()

前者调用 NumPy 函数。

后者调用 ndarray 对象自身的方法。

7.8 今日关于 mean 的错误

曾经认为:

np.mean(a, axis=1)

得到每行求和结果。

但 mean 的含义是平均值,不是求和。

例如:

a = np.array([
    [1, 2, 3],
    [4, 5, 6]
])

b = np.mean(a, axis=1, keepdims=True)

第一行:

(1 + 2 + 3) / 3 = 2

第二行:

(4 + 5 + 6) / 3 = 5

最终:

[[2.]
 [5.]]

shape:

(2, 1)

本章需要重点复习 axis、shape 和 keepdims 三者之间的关系。


第八章:Python 类与面向对象编程

8.1 class:定义类

Python:

class GPU:
    pass

class 用于定义类。

pass 表示空操作,可以在暂时没有具体实现时占位。

对应 C++:

class GPU
{
};

8.2 init:初始化方法

Python:

class GPU:
    def __init__(self, name, memory):
        self.name = name
        self.memory = memory

创建对象:

gpu = GPU("RTX 4090", 24)

创建实例时,Python 会调用初始化方法。

但严格来说,__init__ 是初始化已有新实例的方法,而对象的创建还涉及 __new__。

入门阶段把它类比为 C++ 构造函数的初始化作用即可。

8.3 self:当前对象

例如:

self.name = name

右边的 name 是传入的参数。

左边的 self.name 是当前对象的实例属性。

例如:

gpu1 = GPU("RTX 4090", 24)

gpu2 = GPU("RTX 4080", 16)

两个对象分别保存自己的数据。

self 与 C++ this

C++:

this->memory = memory;

Python:

self.memory = memory

虽然概念类似,但 C++ 的 this 是指针,Python 的 self 是对象引用。

为什么调用时不用写 self?

定义:

def show_info(self):

调用:

gpu.show_info()

Python 会自动将调用该方法的实例作为第一个参数。

从效果上可以理解为:

GPU.show_info(gpu)

8.4 练习 10:Renderer 类

我的原始代码

class Renderer:
    def __init__(self, name, width):
        self.name = name
        self.width = width

    def show_info(self)
    print(f"{self.name}{self.width}")

R1 = Renderer("Eidovelis", 1920)
R1.show_info()

错误一:缺少冒号

def show_info(self)

应该写:

def show_info(self):

错误二:函数体缩进错误

print() 应该属于 show_info() 函数体,因此必须再缩进一级。

修改后的代码

class Renderer:
    def __init__(self, name, width):
        self.name = name
        self.width = width

    def show_info(self):
        print(f"Renderer: {self.name}")
        print(f"Width: {self.width}")


R1 = Renderer("Eidovelis", 1920)
R1.show_info()

运行结果:

Renderer: Eidovelis
Width: 1920

8.5 继承 Inheritance

Python:

class Renderer:
    def show(self):
        print("普通渲染器")


class NeuralRenderer(Renderer):
    pass

其中:

class NeuralRenderer(Renderer):

表示 NeuralRenderer 继承 Renderer。

如果子类没有重写 show(),就可以使用继承来的父类方法。

8.6 方法重写 Override

class Renderer:
    def show(self):
        print("普通渲染器")


class NeuralRenderer(Renderer):
    def show(self):
        print("神经渲染器")

调用:

r = NeuralRenderer()
r.show()

输出:

神经渲染器

这是方法重写。

与 C++ 不同,Python 普通实例方法默认支持动态分派,不需要 virtual 关键字。

8.7 super()

super() 用于按照 Python 的方法解析顺序访问继承体系中的方法。

简单单继承情况下:

class Renderer:
    def __init__(self, name):
        self.name = name


class NeuralRenderer(Renderer):
    def __init__(self, name, quality):
        super().__init__(name)
        self.quality = quality

其中:

super().__init__(name)

调用父类的初始化方法。

作用是复用已经存在的初始化逻辑,避免重复编写。

注意:在多重继承中,super() 遵循 MRO(方法解析顺序),不一定直接指向某个固定父类。

8.8 练习 11:GPU 类继承

我的原始代码

class GPU:
    def __init__(self, name):
        self.name = name

    def show_info(self):
        print(f"GPU:{self.name}")


class NvidiaGPU(GPU):
    def __init__(self, name, memory):
        super().__init__(name)
        self.memory = memory

    def show_info(self):
        print(f"GPU:{self.name}")
        print(f"memory:{self.memory}GB")


gpu = NvidiaGPU("RTX 4090", 24)
gpu.show_info()

运行结果

GPU:RTX 4090
memory:24GB

代码逻辑正确。

输出格式优化

可以改成:

def show_info(self):
    print(f"GPU: {self.name}")
    print(f"Memory: {self.memory} GB")

本题掌握

  • 父类创建。
  • 子类继承。
  • 使用 super() 进行初始化。
  • 增加子类实例属性。
  • 重写实例方法。
  • 实例化子类并调用方法。

与 PyTorch 的联系

定义 PyTorch 神经网络时会遇到:

class CNN(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        return x

其中:

nn.Module

是神经网络模块基类。

掌握 Python 类与继承之后,后续学习这种结构会容易很多。


第九章:模块与异常处理

9.1 什么是模块 Module?

Python 中一个 .py 文件通常就可以作为一个模块。

例如项目目录:

PythonProject/
    math_utils.py
    main.py

math_utils.py:

def add(a, b):
    return a + b

main.py:

import math_utils

result = math_utils.add(10, 20)
print(result)

输出:

30

import 的作用

import math_utils

导入模块。

然后通过:

math_utils.add()

调用模块里的函数。

from import

from math_utils import add

直接导入指定名称。

之后:

add(10, 20)

不再需要加 math_utils.。

as

import numpy as np

表示将导入的 numpy 模块绑定到别名 np。

9.2 try...except 异常处理

Python 可能在运行时发生异常。

例如:

a = 10
b = 0

result = a / b

这里会触发:

ZeroDivisionError

因为除数不能为 0。

使用异常处理:

try:
    result = a / b

except ZeroDivisionError:
    print("除数不能为0")

可以捕获这个异常。

try 的执行过程

  1. Python 尝试执行 try 中的代码。
  2. 如果出现异常,停止执行该 try 块中尚未执行的语句。
  3. 寻找匹配的 except。
  4. 执行对应的异常处理代码。
  5. 如果异常已经处理,继续执行整个结构后面的代码。

常见异常

异常含义ValueError参数值不符合要求TypeError类型或操作不匹配IndexError序列索引越界KeyError字典缺少指定键ZeroDivisionError除以零FileNotFoundError文件不存在NameError名称未定义SyntaxErrorPython 语法错误

9.3 input() 和 int()

input()

name = input("请输入名字:")

读取用户输入。

返回值是字符串。

即使用户输入:

10

返回的也是字符串 "10"。

int()

a = int("10")

将字符串转换为整数。

如果输入不符合整数格式:

int("abc")

会触发 ValueError。

9.4 练习 12:处理列表索引输入

我的原始代码

numbers = [10, 20, 30]

try:
    index = int(input("请输入索引:"))

    result = index
    print(result)

except ValueError:
    print("请输入整数")

except IndexError:
    print("索引超出范围")

主要错误

result = index

这里只是将索引值赋给 result。

假如输入:

1

就输出 1,而不是列表中的 20。

正确做法:

result = numbers[index]

通过索引访问列表。

修改后的代码

numbers = [10, 20, 30]

try:
    index = int(input("请输入索引:"))

    result = numbers[index]
    print(result)

except ValueError:
    print("请输入整数")

except IndexError:
    print("索引超出范围")

三种执行情况

输入:

1

正常输出:

20

输入:

abc

由于 int() 转换失败,触发 ValueError。

程序输出:

请输入整数

输入:

5

由于 numbers[5] 越界,触发 IndexError。

程序输出:

索引超出范围

注意:Python 允许负数索引,因此 -1 对这个列表是合法的。

9.5 异常处理执行顺序

今天完成过一道判断题:

numbers = [10, 20, 30]

try:
    print(numbers[5])

except ValueError:
    print("A")

except IndexError:
    print("B")

print("C")

正确输出:

B
C

原因:

  • 列表访问越界。
  • ValueError 不匹配。
  • IndexError 匹配,输出 B。
  • 异常得到处理。
  • 继续执行后面的 print("C")。

第十章:PyTorch Tensor 入门

10.1 PyTorch 是什么?

PyTorch 是主要面向机器学习与深度学习的计算框架。

它提供:

  • Tensor 张量运算。
  • GPU 加速计算。
  • 自动微分系统。
  • 神经网络模块。
  • 优化器及训练相关工具。

今天只学习了 Tensor 的基础操作,尚未正式学习自动微分。

10.2 Tensor 是什么?

Tensor 可以先理解为多维数组。

NumPy:

import numpy as np

a = np.array([
    [1, 2],
    [3, 4]
])

PyTorch:

import torch

a = torch.tensor([
    [1, 2],
    [3, 4]
])

两者结构类似。

创建一维 Tensor

a = torch.tensor([1, 2, 3])

shape:

torch.Size([3])

创建二维 Tensor

a = torch.tensor([
    [1, 2, 3],
    [4, 5, 6]
])

shape:

torch.Size([2, 3])

10.3 Tensor 的三个重要属性

shape

a.shape

用于查看 Tensor 的形状。

dtype

a.dtype

用于查看 Tensor 的元素类型。

常见类型:

torch.int64
torch.float32
torch.float64

例如:

a = torch.tensor(
    [1, 2, 3],
    dtype=torch.float32
)

这里显式要求创建浮点 Tensor。

device

a.device

用于查看 Tensor 所在的计算设备。

例如:

cpu

或:

cuda:0

具体取决于 Tensor 所在的设备。

10.4 CPU 与 GPU

Tensor 不仅可以放在 CPU 上计算,也可以放在支持的 GPU 上计算。

例如,在 CUDA 环境可用时:

a = a.to("cuda")

将 Tensor 转移至 CUDA GPU。

不过需要确保安装了适合当前 NVIDIA 显卡和软件环境的 PyTorch 构建。

如果 CUDA 不可用,直接移动到 CUDA 设备会报错。

10.5 Tensor 的数学运算

与 NumPy 类似:

a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])

c = a + b

结果:

tensor([5, 7, 9])

也支持标量运算:

a * 3

以及矩阵乘法:

a @ b

对于二维 Tensor,@ 执行矩阵乘法。

10.6 torch.mean()

计算 Tensor 的平均值。

a = torch.tensor([1., 2., 3.])

print(torch.mean(a))

结果:

tensor(2.)

也可以使用对象方法:

a.mean()

注意整数类型

例如:

a = torch.tensor([1, 2, 3])

默认通常得到整数 Tensor。

直接执行:

torch.mean(a)

会因输入不是浮点或复数 Tensor 而报错。

可以改成:

a = torch.tensor(
    [1, 2, 3],
    dtype=torch.float32
)

也可以:

a = a.float()

将数据转换成浮点类型。

10.7 练习 13:第一个 Tensor

我的原始代码

import torch

a = torch.tensor([
    [2, 5, 8],
    [4, 5, 8],
    [9, 5, 8]
])

b = a * 3

print(b.shape)
print(b.dtype)
print(b.device)

问题一:shape 不符合要求

创建的是:

(3, 3)

而题目要求:

(2, 3)

因此要保留两行、每行三个元素。

问题二:遗漏 mean

题目要求计算平均值。

应使用:

torch.mean(b)

问题三:整数 dtype

由于创建 Tensor 时使用了整数,默认类型通常是:

torch.int64

需要改成浮点 Tensor 才能使用 torch.mean()。

修改后的代码

import torch

a = torch.tensor([
    [2, 5, 8],
    [4, 5, 8]
], dtype=torch.float32)

b = a * 3

average = torch.mean(b)

print(b)
print(average)
print(b.shape)
print(b.dtype)
print(b.device)

运行结果

在 CPU 环境下:

tensor([[ 6., 15., 24.],
        [12., 15., 24.]])

tensor(16.)
torch.Size([2, 3])
torch.float32
cpu

本题掌握的知识

  • 使用 torch.tensor() 创建 Tensor。
  • 使用 dtype 指定元素类型。
  • 使用 * 进行逐元素运算。
  • 使用 torch.mean() 求平均值。
  • 查看 shape、dtype、device。

10.8 NumPy 与 PyTorch 对照表

NumPyPyTorch作用np.array()torch.tensor()创建数组或张量.shape.shape获取形状.ndim.ndim获取维度数量.dtype.dtype获取数据类型.reshape().reshape()改变形状np.sum()torch.sum()求和np.mean()torch.mean()求平均值np.max()torch.max()求最大值a @ ba @ b矩阵乘法axis=0dim=0dim=0指定归约维度

PyTorch 的两个重要扩展能力

GPU 运算:

Tensor 可以方便地参与 CUDA 等 GPU 后端的运算。

自动微分:

PyTorch 提供 Autograd 系统,用于计算梯度。

后续会学习:

requires_grad=True
backward()
tensor.grad

这些目前只了解用途,还没有正式进行编程练习。


第十一章:今日错误与纠正汇总

错误原因纠正方式定义函数时忘记参数函数形参与调用实参不匹配正确声明形参for i in (1,n+1)把元组误当连续范围使用 range()偶数求和每次加 2没有累加当前偶数total += i最大值初始值设为 0无法正确处理全负数使用首元素初始化for num in numbers,:逗号使遍历对象变为单元素元组删除逗号把变量命名为 sum遮蔽 Python 内置函数使用 totalnumbers[:6:-1]输出顺序和题目要求不同使用 numbers[-3:]把 (3,) 当成 (1,3)没有区分一维和二维查看 ndim 与 shape对 axis 理解不准确混淆归约方向和结果排列看哪个维度被消除把 mean 当 sum混淆平均值与求和平均值需要除以数量忘记函数冒号Python 语法不熟悉def func():函数体缩进错误没有正确划分作用域函数体再缩进一级result = index只保存索引,没有访问列表result = numbers[index]Tensor shape 不符合要求创建时没有检查维度长度使用 .shape 验证整数 Tensor 求 mean输入 dtype 不符合要求转换为浮点 Tensor

第十二章:复习速查表

Python 基础

# 变量
a = 10

# 输出
print(a)

# 不换行
print(a, end="")

# 条件
if a > 10:
    pass
elif a == 10:
    pass
else:
    pass

# 循环
for i in range(5):
    print(i)

# while
while a > 0:
    a -= 1

# 函数
def add(a, b):
    return a + b

# 类型
type(a)

# 输入
input("请输入:")

# 整数转换
int("10")

Python 数据结构

# 列表
numbers = [1, 2, 3]

numbers.append(4)

numbers[0]

numbers[-1]

numbers[1:3]

numbers[::-1]

len(numbers)

# 元组
position = (1, 2, 3)

# 字典
gpu = {"RTX 4090": 24}

gpu["RTX 4090"]

gpu.items()

gpu.keys()

gpu.values()

# 集合
data = {1, 2, 3}

# 推导式
[i * 2 for i in numbers if i > 1]

NumPy

import numpy as np

a = np.array([
    [1, 2],
    [3, 4]
])

a.ndim
a.shape
a.dtype

a[0, 1]

a.reshape(1, 4)

a * 2

a @ a

np.sum(a)
np.mean(a)
np.max(a)
np.min(a)

np.sum(a, axis=0)

np.mean(a, axis=1, keepdims=True)

Python 类

class GPU:
    def __init__(self, name):
        self.name = name

    def show(self):
        print(self.name)


class NvidiaGPU(GPU):
    def __init__(self, name, memory):
        super().__init__(name)
        self.memory = memory

    def show(self):
        print(self.name, self.memory)

异常处理

try:
    index = int(input("索引:"))
    print(numbers[index])

except ValueError:
    print("输入不是整数")

except IndexError:
    print("索引越界")

PyTorch

import torch

a = torch.tensor([
    [1, 2, 3],
    [4, 5, 6]
], dtype=torch.float32)

a.shape
a.dtype
a.device

b = a * 2

torch.mean(b)

torch.sum(b)

b.reshape(3, 2)

第十三章:学习进度与后续计划

目前已经学习

Python

  • 基础数据类型
  • 变量与运算符
  • 条件判断与循环
  • 函数、参数、return
  • 列表、切片、推导式
  • 元组、字典、集合基础
  • 对象引用
  • 类、对象、继承、方法重写
  • super()
  • import
  • try...except
  • input()

NumPy

  • ndarray
  • ndim、shape、dtype
  • 多维索引
  • 向量化运算
  • 广播机制
  • reshape
  • 矩阵乘法
  • axis
  • keepdims
  • 常用统计函数

PyTorch

  • Tensor 创建
  • shape、dtype、device
  • 基础逐元素运算
  • torch.mean()

尚未系统学习

Python 补充:

文件读写、包的组织、Python 环境管理等内容。

NumPy 补充:

多维切片、布尔索引、transpose、数组拼接、视图与复制等内容。

PyTorch 后续重点:

  1. Autograd 自动微分。
  2. requires_grad。
  3. backward()。
  4. 梯度与链式法则。
  5. 损失函数。
  6. 优化器。
  7. nn.Module。
  8. 神经网络训练循环。
  9. CNN 的 PyTorch 实现。
  10. 图像降噪训练项目。

下一次学习起点

PyTorch 第 2 课:自动微分 Autograd

学习顺序:

导数与梯度 → requires_grad → 计算图 → backward() → .grad → 简单梯度下降。

下一阶段不只要求程序能够运行,还需要能够解释每一行代码在神经网络训练中的作用。


今日总结: 已经从 Python 基础语法学习到 NumPy 的多维数值运算,并开始掌握 PyTorch Tensor。下一阶段将从普通的张量计算过渡到自动微分和神经网络训练。

本记录同时作为后续复习材料,重点回顾练习中的错误及其产生原因。

评论