# 从线性回归到波士顿房价——一个老程序员的机器学习入门实战
从线性回归到波士顿房价——一个老程序员的机器学习入门实战
学完TensorFlow基础,下一步就是拿它做点什么。线性回归、波士顿房价预测、MNIST手写数字——经典三件套。这篇记录我的学习过程,重点关注每个实验教会了我什么,不是教程。
文章目录
一、一元线性回归:y = 2x + 1
TF 1.x版本
生成100个带噪声的数据点,真实关系是y = 2x + 1,让模型自己学出来。
x_data = np.linspace(-1, 1, 100)
y_data = 2 * x_data + 1.0 + np.random.randn(*x_data.shape) * 0.4
w = tf.Variable(1.0, name="w0")
b = tf.Variable(0.0, name="b0")
def model(x, w, b):
return tf.multiply(x, w) + b
loss_function = tf.reduce_mean(tf.square(y - pred))
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss_function)
训练100轮,每轮逐样本喂数据。训练过程中每轮画一条拟合线,可以看到线从歪到正的过程——第一次看到这个动画的时候,直观理解了"梯度下降在做什么"。
训练完支持交互式输入:输入一个x,输出预测值和目标值,直观对比。
学到的东西:
- 损失函数:均方误差(MSE)——预测值和真实值差的平方再求平均。差距越大,梯度越大,参数调整越多。
- 学习率:0.01太小收敛慢,0.1太大可能震荡。这个参数要调。
- 训练方式:逐样本训练(SGD),简单但慢。后来波士顿房价用了mini-batch。
TF 2.x版本
同样的任务,用GradientTape重写:
w = tf.Variable(np.random.randn())
b = tf.Variable(0.0)
def loss(x, y, w, b):
err = model(x, w, b) - y
return tf.reduce_mean(tf.square(err))
def grad(x, y, w, b):
with tf.GradientTape() as tape:
loss_ = loss(x, y, w, b)
return tape.gradient(loss_, [w, b])
for epoch in range(train_epochs):
loss_ = loss(x_data, y_data, w, b)
deltaW, deltaB = grad(x_data, y_data, w, b)
w.assign_sub(deltaW * learning_rate)
b.assign_sub(deltaB * learning_rate)
对比1.x版本:
- 不需要Session、placeholder、feed_dict
- 梯度计算显式写在代码里,能看清每一步在干什么
w.assign_sub()手动更新参数,比1.x的optimizer.minimize()更透明
二、波士顿房价:从一维到多维
一元线性回归只有一个x,波士顿房价有12个特征(犯罪率、房间数、年龄等),预测房价。
TF 1.x版本(boson1.0)
# 12个特征的权重矩阵
w = tf.Variable(tf.random.normal([12, 1], stddev=0.01))
b = tf.Variable(1.0)
def model(x, w, b):
return tf.matmul(x, w) + b # 矩阵乘法,不再是标量乘法
数据归一化:
for i in range(12):
data[:, i] = data[:, i] / (data[:, i].max() - data[:, i].min())
这是手写的min-max归一化。12个特征量级差很大,不归一化训练不收敛。训练1000轮,每轮打乱数据顺序。
问题:没有验证集,只有训练集和测试集,无法判断过拟合。
TF 2.x版本(boson2.0)
升级点:
# sklearn标准化代替手写归一化
x_train = tf.cast(scale(x_train), dtype=tf.float32)
# 三分数据集:训练300条、验证100条、测试剩余
train_num = 300
valid_num = 100
# mini-batch训练
batch_size = 20
total_step = int(train_num / batch_size)
for step in range(total_step):
xs = x_train[step*batch_size:(step+1)*batch_size, :]
ys = y_train[step*batch_szie:(step+1)*batch_size]
grads = grad(xs, ys, w, b)
optimizer.apply_gradients(zip(grads, [w, b]))
同时记录训练loss和验证loss,画两条曲线:
loss_list_train.append(loss_train)
loss_list_valid.append(loss_valid)
plt.plot(loss_list_train, label="train")
plt.plot(loss_list_valid, label="valid")
这一版教会我:
- 数据集要三分——训练集训练参数,验证集调超参数,测试集最终评估。如果只有训练和测试,你不知道模型是学到了规律还是背了答案。
- mini-batch——不是一条一条喂,也不是一把全喂,每次喂一小批(20条)。收敛速度和稳定性的折中。
- 标准化比归一化好——sklearn的
scale()做的是Z-score标准化(减均值除标准差),比min-max更鲁棒。 - 看两条loss曲线——训练loss在降但验证loss开始升了,就是过拟合的信号。
三、MNIST手写数字:自己解析二进制
Keras可以一行加载MNIST,但我额外写了一个从二进制文件手动解析的版本:
def load_images(file_name):
binfile = open(file_name, 'rb')
buffers = binfile.read()
magic, num, rows, cols = struct.unpack_from('>IIII', buffers, 0)
bits = num * rows * cols
images = struct.unpack_from('>' + str(bits) + 'B', buffers, struct.calcsize('>IIII'))
images = np.reshape(images, [num, rows * cols])
return images
MNIST的二进制格式是IDX格式:前4个整数是魔数+数量+行数+列数(大端序),后面是像素数据。用Python的struct模块逐字节解析。
解析完后画出来看看:
for i in range(30):
images = np.reshape(train_images[i], [28, 28])
ax = fig.add_subplot(6, 5, i+1, xticks=[], yticks=[])
ax.imshow(images, cmap=plt.cm.binary, interpolation='nearest')
ax.text(0, 7, str(train_labels[i]))
28×28的灰度图,手写的0-9数字。人一眼就能认出来,但要让机器认出来,需要卷积神经网络(CNN),那是后面的事了。
为什么要手写解析? 因为真实项目的数据不会是Keras帮你准备好的CSV。你的数据可能是Oracle里的表、日志文件、二进制协议、API返回的JSON。手写解析IDX格式就是练习"从原始数据到可用数据"这个能力。
四、三个实验的递进关系
| 实验 | 维度 | 教会我什么 |
|---|---|---|
| 一元线性回归 | 1个特征 | 梯度下降的基本过程、损失函数、学习率 |
| 波士顿房价 | 12个特征 | 多维权重、数据标准化、训练/验证/测试划分、mini-batch、过拟合 |
| MNIST二进制解析 | 784维 | 数据不是现成的、二进制解析、图像表示 |
一元→多元→图像,复杂度逐步上升,但核心都是同一件事:定义模型 → 定义损失 → 求梯度 → 更新参数 → 循环。
这个循环就是机器学习最核心的套路。后面的CNN、Transformer,换了模型结构,没换这个循环。
相关阅读:
- 《一个Java老鸟的TensorFlow入门——从计算图到GradientTape》
- 《一个46岁架构师的AI实战经验总结》
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)