第五章 代码与灵魂
专栏总目录 :《智能重生》AI工程师成长小说专栏
一
陆鸣在赵工程师的工作间里醒来,脸贴在冰凉的桌面上,嘴角还粘着一道干了的记号笔印。白板上的公式被他蹭花了一大片,蓝色和黑色的笔迹混在一起,像某种抽象画。
工作台上多了一样东西------一碗粥,还冒着热气。碗旁边压着一张纸条,是沈莜的字迹,又大又歪:"喝完记得把碗还回来。别自己藏起来当宝贝。"
陆鸣端起碗喝了一口。米粒很稠,不是那种能照见人影的稀粥,而是真正能填肚子的、有分量的粥。他喝了两口,觉得今天的粥特别香------也许是因为加了一点点盐,也许是因为他太累了,胃在发出最诚实的赞美。
盒子亮了。
"第五章:Python编程与数据结构。预计学习时间:6小时(加速模式)。完成本章后,你将能够亲手编写一个完整的神经网络------从数据加载到训练循环,全部由你自己实现。"
陆鸣把最后一口粥喝完,把碗放在一边,深吸一口气。
"开始。"
二
"Python是一种编程语言。它的设计哲学是:简单、明确、优雅。在大断线前的世界,它是AI领域最常用的语言,因为它的语法接近人类自然语言,学习曲线平缓。"
屏幕上出现了第一行代码:
python
print("Hello, world!")
"这是每一个程序员的第一个程序。它在屏幕上输出Hello, world!。"
陆鸣在工作台的终端上------那是一台古老的、但还能运行的电脑------打开了Python解释器。他照着盒子上的代码,一个字母一个字母地敲进去。
>>> print("Hello, world!")
Hello, world!
屏幕回应了他。那种感觉很奇怪------不是"物理世界"的回应,不是力气换来的结果,而是纯粹的、由符号和规则驱动的、看不见摸不着却在眼前发生了的回应。
他像个孩子一样,又敲了一遍。
>>> print("陆鸣是个废物")
陆鸣是个废物
屏幕上出现了他骂自己的话。他看着那行字,嘴角不自觉地歪了一下------连电脑都同意他是废物?不对,电脑只是在重复他告诉它的话。它没有判断,没有感情,只有执行。
这是他第一次感受到"编程"的本质:你给机器精确的指令,它做精确的事。不多,不少,不评价。
"接下来学习变量。"盒子继续。
python
x = 5
y = 3
z = x + y
print(z) # 输出8
"变量是存储数据的容器。Python中的变量不需要声明类型,直接赋值即可。支持的数据类型包括:整数(int)、浮点数(float)、字符串(str)、布尔值(bool)、列表(list)、元组(tuple)、字典(dict)等。"
陆鸣一个一个地试。
python
name = "陆鸣"
age = 22
height = 1.72
is_alive = True
hobbies = ["捡垃圾", "吃营养膏", "睡觉"]
profile = {"name": "陆鸣", "age": 22, "occupation": "回收者"}
他看着那个hobbies列表,苦笑了一下。"捡垃圾"居然是他的爱好。不对------那不是爱好,是生存手段。但他没有更好的词来形容。
"列表是Python中最常用的数据结构之一。它是一个有序的、可变的元素集合。你可以用索引访问元素------索引从0开始。"
python
print(hobbies[0]) # 捡垃圾
hobbies.append("学习AI")
print(hobbies) # ['捡垃圾', '吃营养膏', '睡觉', '学习AI']
他往列表里加了一个新项目。看着"学习AI"出现在"捡垃圾"旁边,他觉得这个列表突然变得不那么可悲了。
"控制流:if语句、for循环、while循环。"
python
if age >= 18:
print("陆鸣是成年人")
else:
print("陆鸣是未成年人")
for hobby in hobbies:
print("我喜欢" + hobby)
count = 0
while count < 5:
print(count)
count = count + 1
他运行了这些代码。屏幕上依次出现了"我喜欢捡垃圾"、"我喜欢吃营养膏"、"我喜欢睡觉"、"我喜欢学习AI"。最后那行让他停了一下------"我喜欢学习AI"。这是真的吗?他真的喜欢吗?还是他只是不得不学?
他不确定。但他知道,当他看到代码按照他的指令正确运行时,心里有一种微弱的、像火星子一样的东西在闪烁。不是快乐,是更安静的东西------满足。
"现在学习函数。函数是组织好的、可重复使用的代码块。"
python
def greet(name):
return "你好," + name
message = greet("陆鸣")
print(message) # 你好,陆鸣
"函数可以接收输入(参数),进行一些操作,然后返回输出。在AI中,模型的前向传播就是一个巨大的函数:输入数据,输出预测。"
陆鸣写了一个自己的函数:
python
def copper_probability(weight, color_score):
# 根据重量和颜色分数估算铜的概率
# 权重凭经验:重量占70%,颜色占30%
return weight * 0.7 + color_score * 0.3
prob = copper_probability(0.8, 0.9)
print(f"这个零件是铜的概率约{prob:.0%}") # 输出:这个零件是铜的概率约83%
他居然用代码把自己捡垃圾的经验写了出来。那一刻,他觉得自己不是在学编程------他是在把自己的大脑翻译成机器能懂的语言。
盒子的声音出现了赞许的意味。"很好。你已经开始用编程解决实际问题。接下来学习一个非常重要的数据结构------NumPy数组。"
屏幕上弹出了一个警告框:"NumPy是Python的科学计算库,是AI的基石。在本终端中已预装。请导入并开始学习。"
python
import numpy as np
# 创建数组
a = np.array([1, 2, 3])
b = np.array([[1, 2], [3, 4], [5, 6]])
print(a.shape) # (3,)
print(b.shape) # (3, 2)
"NumPy数组与Python列表不同:它存储同类型数据,支持向量化运算------对数组的操作会自动应用到每一个元素上,不需要写循环。"
python
arr = np.array([1, 2, 3, 4, 5])
print(arr * 2) # [2 4 6 8 10]
print(arr + 10) # [11 12 13 14 15]
print(np.sqrt(arr)) # [1. 1.414 1.732 2. 2.236]
陆鸣看着这些运算,脑子里突然闪过一个念头------这不就是向量吗?他第一课学的向量,在这里就是NumPy数组。同样的数字列表,同样的运算规则。
"NumPy的核心是多维数组对象ndarray。一个二维数组就是矩阵。你可以做矩阵乘法:"
python
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = np.dot(A, B) # 或 A @ B
print(C)
# [[19 22]
# [43 50]]
他手动验证了一下:1×5+2×7=19,1×6+2×8=22,3×5+4×7=43,3×6+4×8=50。和他在第三章学的矩阵乘法完全吻合。
"计算机正在把你学过的所有数学概念变成可执行的代码。"盒子说,"向量、矩阵、导数------这些抽象的符号,在Python和NumPy中变成了具体的操作。这就是'计算思维':把数学转化为算法,把算法转化为代码。"
赵工程师不知什么时候站在了门口,手里拿着一个保温杯。他看着屏幕上那些代码,推了推眼镜。
"你在学NumPy?"
"嗯。"
"那你差不多可以开始写真正的神经网络了。"赵工程师走进来,从抽屉里翻出一个旧硬盘,"这里面有一个数据集。大断线前的经典------MNIST。六万张手写数字的图片,每张28x28像素。它的标签是0到9的数字。这是AI界的'Hello, world'。"
他把硬盘接到工作台的电脑上,打开了一个文件夹。里面躺着几万个文件,名字像"0_1.png"、"1_23.png"之类的。
"你的任务,"赵工程师说,"是用你学到的知识------Python、NumPy、微积分、线性代数------从零写一个神经网络,能识别这些手写数字。不准用现成的深度学习框架。你要自己实现矩阵乘法、激活函数、反向传播。"
陆鸣盯着屏幕上那些图片。手写数字歪歪扭扭,有的写得像鬼画符,有的勉强能认出来。人类一眼就能看出那是几,但机器需要被教会------用数学、用代码、用无数次的计算。
"如果我写不出来呢?"
"那你学的一切就只是纸上谈兵。"赵工程师喝了一口保温杯里的水,"AI不是数学竞赛。AI是在真实数据上运行的代码。不懂代码,你就只是一个会说几个名词的......废物。"
这个词像一把小刀,准确地扎进了陆鸣的胸口。
他转过身,面对着屏幕。
"开始写。"
三
盒子上出现了一个教学界面,左边是任务说明,右边是代码编辑器。
"实现一个最简单的神经网络:输入层784个神经元(28x28像素),隐藏层128个神经元,输出层10个神经元(对应数字0-9)。激活函数:隐藏层使用ReLU,输出层使用Softmax。损失函数:交叉熵损失。优化算法:随机梯度下降(SGD),学习率0.01。"
陆鸣觉得自己像是被扔进了一个深渊。他只知道这些名词,但要把它们变成代码,像把一堆砖头变成一座房子。
"一步一步来。"盒子说,"第一步:加载数据。"
python
import numpy as np
import struct
def load_mnist_images(filename):
with open(filename, 'rb') as f:
magic, num, rows, cols = struct.unpack('>IIII', f.read(16))
images = np.fromfile(f, dtype=np.uint8).reshape(num, rows * cols)
return images.astype(np.float32) / 255.0 # 归一化到0-1
def load_mnist_labels(filename):
with open(filename, 'rb') as f:
magic, num = struct.unpack('>II', f.read(8))
labels = np.fromfile(f, dtype=np.uint8)
return labels
X_train = load_mnist_images('train-images.idx3-ubyte')
y_train = load_mnist_labels('train-labels.idx1-ubyte')
X_test = load_mnist_images('t10k-images.idx3-ubyte')
y_test = load_mnist_labels('t10k-labels.idx1-ubyte')
陆鸣看着这些代码,大部分细节他还不完全懂------那个struct.unpack是什么?为什么要用>IIII?但他理解了核心思路:把图片文件读进来,把像素值变成0到1之间的浮点数,然后用一个二维数组存储,每一行是一张图片,每一列是一个像素。
"第二步:初始化参数。"
python
input_size = 784
hidden_size = 128
output_size = 10
# He初始化:权重用均值为0、方差为2/输入维度的正态分布
W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size)
b1 = np.zeros((1, hidden_size))
W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size)
b2 = np.zeros((1, output_size))
"为什么权重不初始化为零?因为如果所有权重相同,所有神经元将学习到相同的特征,网络无法打破对称性。为什么用这个特定的缩放因子?这是He初始化,适用于ReLU激活函数。"
陆鸣把这些记在心里。每一条看似随意的代码背后,都有一个数学理由。
"第三步:前向传播与激活函数。"
python
def relu(z):
return np.maximum(0, z)
def softmax(z):
exp_z = np.exp(z - np.max(z, axis=1, keepdims=True)) # 减去最大值防止溢出
return exp_z / np.sum(exp_z, axis=1, keepdims=True)
# 前向传播
def forward(X, W1, b1, W2, b2):
Z1 = np.dot(X, W1) + b1 # 线性变换
A1 = relu(Z1) # ReLU激活
Z2 = np.dot(A1, W2) + b2 # 线性变换
A2 = softmax(Z2) # Softmax激活,输出概率分布
return Z1, A1, Z2, A2
他运行了一下,用几张图片测试前向传播,输出是一些随机的概率------因为还没有训练,模型的预测完全是瞎猜。
"第四步:损失函数。交叉熵损失衡量预测分布与真实分布的差距。"
python
def cross_entropy_loss(y_pred, y_true):
m = y_true.shape[0]
# y_true是标签(0-9的数字),需要转换为one-hot编码
y_true_one_hot = np.zeros((m, output_size))
y_true_one_hot[np.arange(m), y_true] = 1
# 计算交叉熵: -Σ y_true * log(y_pred)
log_likelihood = -np.log(y_pred + 1e-8) # 加1e-8避免log(0)
loss = np.sum(y_true_one_hot * log_likelihood) / m
return loss
"第五步:反向传播。这是核心。你需要计算损失对每个参数的梯度。"
陆鸣觉得自己的大脑在燃烧。他需要运用链式法则------从损失开始,一步一步往回推。
盒子上给出了公式:
对于输出层(Softmax + 交叉熵的梯度简化形式):
dZ2 = y_pred - y_true_one_hot
对于隐藏层到输出层的权重和偏置:
dW2 = (1/m) * A1.T @ dZ2
db2 = (1/m) * np.sum(dZ2, axis=0, keepdims=True)
对于隐藏层的梯度(使用ReLU的导数:大于0时导数为1,否则为0):
dA1 = dZ2 @ W2.T
dZ1 = dA1 * (Z1 > 0) # ReLU导数
dW1 = (1/m) * X.T @ dZ1
db1 = (1/m) * np.sum(dZ1, axis=0, keepdims=True)
他一个字母一个字母地敲:
python
def backward(X, y_true, y_pred, Z1, A1, W2):
m = X.shape[0]
y_true_one_hot = np.zeros((m, output_size))
y_true_one_hot[np.arange(m), y_true] = 1
# 输出层梯度
dZ2 = y_pred - y_true_one_hot
dW2 = (1/m) * A1.T @ dZ2
db2 = (1/m) * np.sum(dZ2, axis=0, keepdims=True)
# 隐藏层梯度
dA1 = dZ2 @ W2.T
dZ1 = dA1 * (Z1 > 0) # ReLU的导数
dW1 = (1/m) * X.T @ dZ1
db1 = (1/m) * np.sum(dZ1, axis=0, keepdims=True)
return dW1, db1, dW2, db2
"第六步:参数更新------梯度下降。"
python
learning_rate = 0.01
def update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, lr):
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
return W1, b1, W2, b2
"第七步:训练循环。"
python
epochs = 20
batch_size = 64
for epoch in range(epochs):
# 每个epoch打乱数据
indices = np.random.permutation(X_train.shape[0])
X_shuffled = X_train[indices]
y_shuffled = y_train[indices]
total_loss = 0
for i in range(0, X_train.shape[0], batch_size):
X_batch = X_shuffled[i:i+batch_size]
y_batch = y_shuffled[i:i+batch_size]
# 前向传播
Z1, A1, Z2, y_pred = forward(X_batch, W1, b1, W2, b2)
# 计算损失
loss = cross_entropy_loss(y_pred, y_batch)
total_loss += loss
# 反向传播
dW1, db1, dW2, db2 = backward(X_batch, y_batch, y_pred, Z1, A1, W2)
# 更新参数
W1, b1, W2, b2 = update_parameters(W1, b1, W2, b2, dW1, db1, dW2, db2, learning_rate)
# 每个epoch结束后在测试集上评估准确率
_, _, _, test_pred = forward(X_test, W1, b1, W2, b2)
test_acc = np.mean(np.argmax(test_pred, axis=1) == y_test)
print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/(X_train.shape[0]//batch_size):.4f}, Test Acc: {test_acc:.4f}")
他按下了运行键。
屏幕上的光标闪烁了一下,然后开始输出:
Epoch 1/20, Loss: 0.8473, Test Acc: 0.8524
Epoch 2/20, Loss: 0.3982, Test Acc: 0.8935
Epoch 3/20, Loss: 0.3276, Test Acc: 0.9062
Epoch 4/20, Loss: 0.2851, Test Acc: 0.9158
Epoch 5/20, Loss: 0.2584, Test Acc: 0.9231
...
Epoch 20/20, Loss: 0.1527, Test Acc: 0.9589
95.89%的准确率。一个从零写出来的、没有任何深度学习框架的、只有一层隐藏层的神经网络,识别手写数字的准确率达到了95%以上。
陆鸣靠在椅背上,盯着屏幕上那个"0.9589",感觉自己的胸腔里有什么东西在膨胀。不是骄傲------是震撼。他亲手创造了一个能"看"的东西。它没有眼睛,没有意识,但它能从像素里看出数字。它学会了。
他低头看着自己的手。这双手,曾经只能在垃圾堆里翻出铜和铁。现在,这双手写了200行代码,创造了一个能识别手写数字的人工神经网络。
"我做到了。"他说。
赵工程师在他身后,一直没有说话。但现在,他开口了,声音很轻:
"这只是开始。"
四
那天深夜,工作间里只剩下陆鸣和盒子。赵工程师回去了,临走前在陆鸣桌上放了一个新的保温杯,里面是热的水。
陆鸣没有睡意。他一遍又一遍地运行他的神经网络,调整学习率、改变隐藏层大小、尝试不同的初始化方法、添加Dropout、实验不同的优化器(他手动实现了带动量的SGD)。他像是一个刚拿到新玩具的孩子,迫不及待地想拆开它、弄懂它、改进它。
盒子上出现了一个他从未见过的界面。不是教学课程,而是一个问题:
"你已经学会了神经网络的基础实现。你理解了前向传播、反向传播、梯度下降。你甚至可以自己调优模型。但你只看到了AI的'壳'。你想看到'核'吗?"
陆鸣的手指悬停在屏幕上。
"什么'核'?"
"'天工'的底层训练框架。大断线前,'天工'的核心训练代码是开源的。我现在可以让你访问一份拷贝。你可以在沙盒环境中阅读它的代码,理解一个超级智能是如何被训练出来的。这不是教学------这是真正的工业级代码。数百万行,涉及分布式训练、混合精度、梯度累积、模型并行......你可能会被吓到。"
陆鸣沉默了很久。
他想起了白天写的200行代码。那是他的骄傲。但和"天工"的数百万行相比,那只是一个婴儿的蹒跚学步。他还没有准备好。
但也许,"准备"这件事,永远不会完成。
"给我看。"他说。
屏幕暗了一下,然后出现了第一行代码------不,不是第一行,是文件列表。成百上千个文件,目录结构深不见底。文件名像天书:distributed_trainer.py、sharding_strategy.py、gradient_checkpointer.py......
陆鸣随便打开了一个文件。
python
class ShardedModelPipeline:
"""
Implements model parallelism with automatic sharding.
Supports pipeline parallelism with microbatch scheduling.
"""
def __init__(self, world_size, micro_batch_size=1):
self.world_size = world_size
self.micro_batch_size = micro_batch_size
...
他能看懂一些关键词------model parallelism(模型并行),microbatch(微批次)。他在盒子的课程里听说过这些概念:当模型太大,无法放进单个GPU的内存时,就需要把模型切分到多个设备上。但具体的实现细节像一座大山,压在他的认知边界上。
他没有退缩。他开始一行一行地读。读不懂的地方,他就问盒子。盒子就像一个耐心的导师,解释每一个术语、每一段逻辑、每一个设计决策背后的考量。
凌晨三点,他读完了分布式训练的概览。他知道了数据并行和模型并行的区别,知道了环形全归约(Ring All-Reduce)算法,知道了同步训练和异步训练各自的优缺点。
他的知识星图在便携终端上爆炸式地扩张。那些光点不再是散落的星星,而是连成了星座,星座连成了星云,星云连成了银河。每一片区域都有名字:线性代数、微积分、概率论、Python、NumPy、机器学习基础、神经网络、反向传播、优化算法、分布式训练......
他不再是废物。他甚至不再是一个"初学者"。他已经站在了AI工程师的门槛上,一只脚已经迈了进去。
盒子在凌晨四点发出了一条消息:
"第五章完成情况:Python基础(100%)、NumPy数组与向量化(100%)、神经网络从零实现(95%)、读懂工业级代码(30%)。综合评分:B+。"
"剩余课程进度:45%。"
"用户心肺功能监测:心率偏高,血压偏高,建议休息。"
陆鸣没有休息。他合上盒子,走到窗边,看着净土地的夜色。电磁屏障的蓝光在天幕上投下淡淡的光晕,像极光一样柔和。能源核心的排热口吐着稳定的红光,像大地的心跳。
他掏出便携终端,点亮了知识星图。那些密密麻麻的光点,每一个都是他亲手点亮的------不是靠天赋,而是靠笨功夫。一遍看不懂就看两遍,两遍看不懂就看十遍。他用的是在垃圾堆里翻东西的劲儿------不放弃,不跳过,一个一个地分辨。
"你还不睡?"门口传来沈莜的声音。她披着一件旧外套,手里拿着一个饭盒。
"你怎么来了?"
"老赵说你在这儿通宵,让我送点吃的。你不是说要终身免费粥票吗?先拿这个顶一下。"她把饭盒放在桌上,打开盖子。里面是粥,还有两块腌萝卜------净土地的奢侈品。
陆鸣坐下来,慢慢地喝粥。萝卜的咸味在舌尖扩散,带着一种久违的、像"家"的味道。
"沈莜。"
"嗯?"
"你说,一个人如果突然变得......不一样了,是因为他本来就有那个潜力,还是因为外界逼的?"
沈莜靠在门框上,想了想。"我觉得,潜力这东西,就像垃圾堆里的铜。你不去翻,它永远埋在底下。但翻的人多了,总有人能翻到。你不是突然变聪明了,你是终于开始翻了。"
她说完就走了。
陆鸣喝完粥,把饭盒洗干净,放在工作台上。他看了一眼那块写着"废物回收与AI咨询"的招牌------那是他打算以后开的店。招牌还没有做,但名字已经想好了。
他躺在那张硬邦邦的折叠床上,盒子和书并排放在枕头边。
闭上眼睛之前,他看到了知识星图上最后一颗刚刚亮起来的星。它的标签是:
"分布式训练------Ring All-Reduce 算法原理。"
他还远远没有学完。但至少,他知道自己该往哪个方向走了。
第五章 · 完
本章知识清单:
- Python基础:变量、数据类型、列表、字典、控制流(if/for/while)、函数定义与调用
- NumPy核心:ndarray多维数组、向量化运算、数组切片、矩阵乘法(@或dot)
- 数据预处理:图像归一化、训练/测试集划分、批次迭代
- 神经网络前向传播:线性变换 Z = WX+b,ReLU激活,Softmax输出
- 损失函数:交叉熵------衡量预测分布与真实分布的差异
- 反向传播(核心):链式法则应用于神经网络各层,计算梯度
- 参数初始化:He初始化(针对ReLU),打破对称性
- 优化算法:小批量随机梯度下降(SGD),学习率调度
- 训练循环:迭代epoch、批处理、打乱数据、记录损失与准确率
- 工业级AI的扩展:分布式训练(数据并行、模型并行)、混合精度、梯度累积
编程作业建议(读者可自行尝试):
- 实现本章描述的MNIST分类器,在自己的电脑上运行(需要Python和NumPy)
- 尝试调整隐藏层大小、学习率、batch size,观察准确率变化
- 尝试添加第二个隐藏层(变成两层隐藏层),观察效果
下一章预告:第六章《从感知到认知》
陆鸣将进入深度学习的高级主题:卷积神经网络(CNN)和循环神经网络(RNN)。CNN用于图像识别,RNN用于序列数据(如文本、时间序列)。他将在净土地的实际问题中应用这些模型------识别废墟中的危险机器类型(CNN),以及预测能源核心的负载变化(RNN)。同时,他将首次接触"预训练模型"的概念,了解迁移学习如何让AI在数据稀缺时也能发挥作用。