TensorFlow实战

更新时间: 2026-07-10 08:25:22

# 什么是“张量 (Tensor)”

你在 Numpy 里用的 np.array,在 TensorFlow 里就叫“张量”。

  • 标量:就是一个数(比如房价 300万)。
  • 向量:就是一排数(比如房子的 [面积, 房间数, 楼层])。
  • 矩阵:就是一张表(比如 100 套房子的数据表)。
  • 高维张量:就是好多张表叠在一起(比如彩色图片,有长、宽、颜色三个维度)。

为什么要换个名字?
因为 Numpy 的数组只能在 CPU(中央处理器)上跑,速度慢。而 TensorFlow 的“张量”天生就是为了 GPU(显卡) 设计的,它知道怎么把数据切碎了分给几千个显卡核心同时算。

# 什么是“计算图 (Graph)”

回想你的 Numpy 代码:
你是写一行代码,电脑就立刻算一行(比如先算乘法,再算加法,再算激活函数)。这叫“解释执行”,就像你一边看图纸一边砌砖,砌一块看一块。

TensorFlow 的逻辑(图模式):
它不急着算。它先让你把所有的公式写下来:

  1. 先把输入X 乘以权重W;
  2. 再加上偏置b ;
  3. 再过一遍 ReLU 函数...

把这些步骤全部画成一张流程图(这就是计算图)。

好处是什么?
一旦图纸画好了,工厂(C++底层引擎)就可以开始优化了。比如它会发现:“哎?这一步算完紧接着就要用,那我就不存硬盘了,直接放内存里传给下一步”,或者“这两步互不干扰,我让两个显卡核心同时算”。这就比 Numpy 那种“走一步看一步”快得多。

# 什么是“会话 (Session)”

在 TensorFlow 1.x(老版本)里,你画好了图纸(计算图),如果不喊一声“开始!”,机器是绝对不会动的。
那个喊“开始”并负责分配显卡内存、把数据喂进去的动作,就叫 Session(会话)。

现在的 TensorFlow 2.x 变了!
你现在写 a = 2, b = 3, print(a+b),它立刻就会打印出 5。不需要 Session,不需要显式画图。

那“计算图”死了吗?没有!
@tf.function这是一个神奇的开关。

  • 平时开发时:你用“即时执行”,怎么爽怎么写,随时 print 检查数据。

  • 准备上线/训练太慢时:你在函数头上加一行 @tf.function。

    • TensorFlow 会在后台偷偷把你写的 Python 代码自动翻译成“计算图”。
    • 然后利用我们前面说的“图优化”加速。

# 波士顿房价预测

# 第一部分:环境准备与数据读取

import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ================= 1. 数据读取与预处理 =================
try:
    # 尝试读取文件,兼容空格分隔符
    # sep='\s+' 表示使用任意空白字符(空格、Tab等)作为分隔符,防止格式报错
    df = pd.read_csv('housing.csv', sep='\s+', header=None)
    
    # .values 将 DataFrame 转为 Numpy 数组,.astype('float32') 强制转为单精度浮点数
    # 【重点】TensorFlow 默认使用 float32 计算,提前转换可以避免后续类型不匹配的报错,还能节省显存
    data = df.values.astype('float32')
except FileNotFoundError:
    print("错误:未找到 housing.csv 文件,请确保文件在当前目录下。")
    exit()
except Exception as e:
    print(f"读取文件时发生未知错误:{e}")
    exit()

print(f"数据形状: {data.shape}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • tensorflow:核心库。
  • layers:这是“积木块”,比如全连接层(Dense)、卷积层等。
  • models:这是“容器”,用来把积木搭成模型。

补充知识:在 TF2.0 中,tf.keras 是官方推荐的高级 API。以前大家还要纠结用 Keras 还是 TF,现在它们合体了,直接用 tf.keras 就行。

为什么用 float32?
神经网络的权重默认是 32 位浮点数。如果输入数据是 float64(Pandas 默认),模型在计算时会不断进行类型转换,不仅慢,还容易报 TypeError。

防御性编程 (try-except):
在处理本地文件时,永远不要假设文件一定存在。加上异常捕获能让你的代码更健壮,避免直接抛出红色的报错堆栈吓跑用户。

# 第二部分:数据集划分(黄金法则)

# ================= 2. 数据集划分 =================
# 设置随机种子,保证每次运行打乱的顺序一致,方便复现结果
np.random.seed(42)

# np.random.permutation(n) 生成 0 到 n-1 的随机排列索引
indices = np.random.permutation(data.shape[0])
# 根据随机索引重新排列数据,这一步叫“洗牌”,防止数据按某种规律排序影响训练
data = data[indices]

X = data[:, :-1]  # 切片操作:取所有行,除了最后一列的所有列 -> 特征
Y = data[:, -1]   # 切片操作:取所有行,最后一列 -> 目标值(房价)

# --- 第一步:分出测试集 (20%) ---
# 测试集是“期末考试卷”,必须最先切出来,且在整个训练过程中绝对不能碰
split_idx = int(X.shape[0] * 0.8)
X_train_full, X_test = X[:split_idx], X[split_idx:]
Y_train_full, Y_test = Y[:split_idx], Y[split_idx:]

# --- 第二步:从剩余数据中分出验证集 (20% of 80%) ---
# 验证集是“模拟考”,用来在训练过程中监控模型是否过拟合
val_split_idx = int(X_train_full.shape[0] * 0.8)
# 注意:这里把前 80% 给训练,后 20% 给验证(顺序不影响,只要不重叠即可)
X_val, X_train_real = X_train_full[val_split_idx:], X_train_full[:val_split_idx]
Y_val, Y_train_real = Y_train_full[val_split_idx:], Y_train_full[:val_split_idx]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

为什么要分三份数据?

  • 训练集:课本知识,用来学习参数。
  • 验证集:平时测验,用来调整超参数(如学习率、层数)和判断何时停止训练。
  • 测试集:高考,只在最后用一次,用来评估模型的真实性能。

洗牌的必要性:

  • 原始数据可能按时间或价格排序。如果不洗牌,模型可能只学到了“后面的数据比前面的大”这种简单规律,而不是特征与房价的关系。

# 第三部分:数据标准化

# ================= 3. 数据标准化(关键修复点) =================
# 【铁律】统计量(均值 mean / 标准差 std)只能从“训练集”中计算!
# 严禁使用测试集或全局数据的统计量,否则会导致“数据泄露”

# --- 3.1 特征 X 标准化 ---
mean_x = X_train_real.mean(axis=0) # axis=0 表示按列计算均值
std_x = X_train_real.std(axis=0)   # 按列计算标准差

# 防御性编程:如果某列数据完全一样(标准差为0),除以0会报错
# 这里将其设为一个极小值 1e-8,避免程序崩溃
std_x[std_x == 0] = 1e-8 

# 执行标准化公式:(x - mean) / std
X_train_real = (X_train_real - mean_x) / std_x
# 验证集和测试集必须使用【训练集】算出来的 mean 和 std 进行处理
X_val = (X_val - mean_x) / std_x
X_test = (X_test - mean_x) / std_x

# --- 3.2 标签 Y 标准化 ---
# 很多新手只标准化 X,忘了标准化 Y。
# 如果 Y 是 5~50 的大数,而 X 是 0~1 的小数,模型很难收敛。
mean_y = Y_train_real.mean()
std_y = Y_train_real.std()
if std_y == 0: std_y = 1e-8

Y_train_real = (Y_train_real - mean_y) / std_y
Y_val = (Y_val - mean_y) / std_y
Y_test = (Y_test - mean_y) / std_y  # 测试集也要标准化,用于计算 Loss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

什么是 Z-Score 标准化?

  • 公式:z=(x−μ)/σ。它将数据转化为均值为 0,标准差为 1 的分布。
  • 作用:让不同量纲的特征(如房间数 1-10,面积 500-5000)处于同一水平线,加速梯度下降收敛。

为什么要标准化 Y(标签)?
神经网络的激活函数(如 Sigmoid/Tanh)或输出层对大数值敏感。如果目标是预测 500,000,模型输出的微小误差平方后(MSE)会变成天文数字,导致梯度爆炸。将 Y 缩小到 -1 到 1 之间,训练会非常丝滑。

什么是数据泄露?
如果你用了测试集的均值来标准化训练集,相当于考试前偷看了答案的平均分。这会导致评估结果虚高,模型上线后失效。

# 第四部分:构建高效数据管道

# ================= 4. 构建数据管道 =================
BATCH_SIZE = 32

# 使用 tf.data API 构建高性能管道
# from_tensor_slices: 将 Numpy 数组切片成 Tensor
train_dataset = tf.data.Dataset.from_tensor_slices((X_train_real, Y_train_real))

# shuffle: 再次打乱(防止上一个 Epoch 结尾的数据和下一个 Epoch 开头的数据有相关性)
# buffer_size=1000: 缓冲区大小,越大越乱,但也越占内存
# batch: 打包,每次取 32 条数据
# prefetch: 预取,CPU 准备下一批数据时,GPU 正在训练当前批次,实现并行加速
train_dataset = train_dataset.shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)

# 验证集不需要 shuffle,也不需要太大缓冲
val_dataset = tf.data.Dataset.from_tensor_slices((X_val, Y_val))
val_dataset = val_dataset.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)

test_dataset = tf.data.Dataset.from_tensor_slices((X_test, Y_test))
test_dataset = test_dataset.batch(BATCH_SIZE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

为什么要用 tf.data 而不是直接传 Numpy 数组?
当数据量很大(几个 G)时,Numpy 数组会撑爆内存。tf.data 支持流式读取。
Prefetch(预取):这是提速的关键。它利用了 CPU 和 GPU 的空闲时间 overlap,通常能提升 20%-50% 的训练速度。

# 第一部分:训练集管道(全自动高效流水线)

  1. 原材料入库:from_tensor_slices
train_dataset = tf.data.Dataset.from_tensor_slices((X_train, Y_train))  
1
  • from_tensor_slices 就像把一大堆原材料(你的 Numpy 数组)切成了一个个独立的样本,放进了传送带的起点。
  • 底层原理:它并没有把数据复制到内存的另一个地方,而是建立了一个指向你 Numpy 数组的“索引”。当你从管道里取数据时,它才会去 Numpy 数组里拿。
  1. 核心三连:打乱、分批、预取
train_dataset = train_dataset.shuffle(buffer_size=1000).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)  
1
  • .shuffle(buffer_size=1000) —— 随机洗牌
    作用:防止模型记住数据的顺序。

知识点(蓄水池洗牌算法):

你可能会问:“老师,为什么是 buffer_size=1000?为什么不直接全量打乱?” 如果你的数据有 1000 万条,全量打乱需要极大的内存。tf.data 的做法是:它先往一个只能装 1000 条数据的“蓄水池”里装满数据,然后随机从中抽取一条发出去,再从原始数据里拿一条新的补进蓄水池。 注意:buffer_size 越大,洗牌越均匀,但消耗内存越多。1000 是一个常用的经验值。

  • .batch(BATCH_SIZE) —— 打包成箱
    作用:把单个样本打包成一批一批的(比如每 32 个样本打包成一批)。

知识点(矩阵运算的威力):

神经网络最擅长的是矩阵运算。计算 1 个样本的梯度,和计算 32 个样本的梯度,在 GPU 上花费的时间几乎是一样的(因为 GPU 是并行计算的)。如果不打包,一个一个喂给 GPU,GPU 大部分时间都在“等数据”,效率极低。

  • .prefetch(tf.data.AUTOTUNE) —— 异步预取(性能黑科技)
    作用:让 CPU 和 GPU 同时工作,消除等待时间。

知识点(流水线并行):

这是你之前问过的重点,我们再用底层逻辑讲透它。 在训练时,有两个主要步骤: 数据预处理(CPU 做):从硬盘读取、打乱、打包。
模型计算(GPU 做):前向传播、反向传播。
如果没有 prefetch,CPU 和 GPU 是串行的:GPU 算完一批,停下来等 CPU 准备下一批;CPU 准备好,停下来等 GPU 算完。
加上 prefetch 后,变成了并行:GPU 正在算第 N 批数据时,CPU 已经在后台偷偷把第 N+1 批数据准备好放在内存里了。AUTOTUNE 会让 TensorFlow 根据你的机器性能,自动决定提前预取几批数据最合适。

# 第二部分:测试集管道(严谨的质检流水线)

test_dataset = tf.data.Dataset.from_tensor_slices((X_test, Y_test))
test_dataset = test_dataset.batch(BATCH_SIZE)  
1
2
  1. 为什么测试集没有 .shuffle()?
    讲解:测试集的目的是“客观评估”和“查看具体预测结果”。如果打乱了,你拿到的预测结果就会和原始数据的顺序对不上号,导致你无法分析到底是哪一套房子预测错了。

  2. 为什么测试集没有 .prefetch()?
    讲解:prefetch 是为了加速反复迭代的训练过程。而测试集(model.evaluate 或 model.predict)通常只在训练结束后跑一遍。为了代码的简洁性,通常不加 prefetch。当然,如果你的测试集非常大(比如几百万条),加上 prefetch 也能加快评估速度,但这不是必须的。

# 第五部分:模型构建与训练

# ================= 5. 模型构建与训练 =================
model = tf.keras.Sequential([
    # 输入层+隐藏层1:64个神经元,ReLU激活函数(解决梯度消失,计算快)
    layers.Dense(64, activation='relu', input_shape=(X_train_real.shape[1],)),
    
    # 隐藏层2:加深网络,提取更复杂的非线性特征
    layers.Dense(64, activation='relu'),
    
    # 输出层:1个神经元。回归问题通常不加激活函数(线性输出),或者范围不限
    layers.Dense(1) 
])

model.compile(
    # Adam 优化器:自适应学习率,新手首选
    # 0.0001 是一个比较保守且安全的学习率,配合标准化数据效果很好
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001), 
    
    loss='mse',       # 均方误差,回归任务的标准损失函数
    metrics=['mae']   # 平均绝对误差,比 MSE 更直观(单位与房价一致)
)

history = model.fit(
    train_dataset,        # 传入处理好的数据集
    epochs=100,           # 训练 100 轮
    validation_data=val_dataset, # 每轮结束后用验证集测一下
    verbose=1             # 显示进度条
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • Sequential:像盖楼一样,一层压一层,适合这种简单的单线结构。
  • Dense(64, ...):这就是你 Numpy 里的 W⋅X+b 。64 代表这一层有 64 个神经元(也就是 64 个权重向量)。
  • input_shape=(X_train.shape[1],):告诉第一层,进来的数据有 13 列特征。后面的层不需要写,因为它能自动推导。
  • 最后一层:注意!这里没有写 activation。因为我们要预测房价(具体数值),如果用 Sigmoid 或 ReLU 会把数值限制住(比如 ReLU 不能预测负数,Sigmoid 只能在 0-1 之间)。回归问题的输出层通常是纯线性的。

为什么选 ReLU?
相比 Sigmoid,ReLU ( f(x)=max(0,x) ) 在正区间导数恒为 1,有效缓解了深层网络中的梯度消失问题,且计算只需一个阈值判断,速度极快。

MSE vs MAE:

  • MSE (Loss):对大误差惩罚重(平方级),适合做优化目标。
  • MAE (Metric):线性误差,比如误差是 2000 块,MAE 就是 2000,人类更容易理解。

# 第六部分:结果可视化与反标准化评估

# ================= 6. 结果可视化 =================
plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文乱码
plt.rcParams['axes.unicode_minus'] = False   # 解决负号显示问题

plt.figure(figsize=(10, 6))
plt.plot(history.history['loss'], label='Training Loss', color='blue')
plt.plot(history.history['val_loss'], label='Validation Loss', color='orange', linestyle='--')
plt.title('Model Loss During Training')
plt.xlabel('Epochs')
plt.ylabel('Loss (MSE)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# ================= 7. 评估与预测(含反标准化) =================
# 在标准化空间内评估(此时得到的 MAE 是小数,比如 0.2,没有物理意义)
test_loss, test_mae_norm = model.evaluate(test_dataset, verbose=0)

# 【关键步骤】将指标还原为真实货币单位
# 公式:真实值 = 标准化值 * std + mean
test_mae_real = test_mae_norm * std_y

print(f"\n【模型考核报告】")
print(f"测试集 Loss (MSE): {test_loss:.4f} (标准化空间)")
print(f"测试集 MAE: {test_mae_real:.2f} (真实房价单位,约 {test_mae_real*1000:.0f} 美元)")

# 预测前5套房子
predictions_norm = model.predict(X_test[:5])

# 【关键】将预测结果还原为真实房价
predictions_real = predictions_norm * std_y + mean_y

# 真实标签也要还原(因为 Y_test 也是标准化过的)
Y_test_real = Y_test[:5] * std_y + mean_y

print(f"\n【前5套房子预测结果展示】")
for i in range(5):
    real_price = Y_test_real[i]
    pred_price = predictions_real[i][0]
    error = abs(real_price - pred_price)
    print(f"真实房价: {real_price:6.2f} | 模型预测: {pred_price:6.2f} | 误差: {error:6.2f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

反标准化:
模型是在“缩微世界”里学习的,输出的也是缩微世界的数值。我们必须用之前保存的 mean_y 和 std_y 把它“放大”回现实世界,这样的预测结果(如 23.5 千美元)才有业务价值。

Loss 曲线分析:

  • 如果 Train Loss 降,Val Loss 升 -> 过拟合(死记硬背)。

  • 如果 Train Loss 和 Val Loss 都降且靠得近 -> 完美(泛化能力强)。

  • 如果两条线都不降 -> 欠拟合(模型太笨或学习率太低)。

  • evaluate:用从未见过的测试集跑一遍,得到最终的 Loss 和 MAE。

  • predict:输入特征,输出预测值。注意输出的形状通常是 [[val1], [val2]],用 .flatten() 可以把它变成一维数组 [val1, val2],方便对比。

关于 model.predict 和 model.evaluate 的区别

这是新手最容易混淆的地方。简单来说:一个是“阅卷打分”,一个是“实战演练”。

为什么前面考核过了,还要再预测?

  • model.evaluate (阅卷打分)

    • 作用:它的目的是算出一个分数(比如 Loss 是 0.5,MAE 是 3.2)。
    • 输出:它只给你返回数字。
    • 场景:用来判断模型“整体表现好不好”。比如你告诉老板:“我们的模型平均误差是 3000 块。”
  • model.predict (实战演练)

    • 作用:它的目的是拿到具体的预测结果。
    • 输出:它返回的是具体的数值列表(比如 [30.5, 22.1, ...])。
    • 场景:这才是模型真正上线干活的时候。比如用户输入了一套房子的数据,系统必须吐出一个具体的价格给用户看,而不是吐出一个“误差值”。
  • 为什么要取 x_test[:5]?

    • 因为测试集可能有 100 条数据,全部打印出来太乱了。[:5] 是 Python 的切片语法,意思是“只取前 5 个数据”来看看效果,方便我们在控制台快速检查。

Loss 曲线丝滑下降:蓝线(训练 Loss)平稳下降并趋于收敛,说明学习率(0.0001)非常合适,模型在稳步学习。
泛化能力极佳:黄线(验证 Loss)紧紧跟随蓝线,没有出现大幅反弹(过拟合),说明模型学到的规律是通用的,而不是死记硬背。 预测精准度质变: 之前:真实 22.40 -> 预测 0.71 (误差 21.69) ❌ 现在:真实 22.40 -> 预测 23.38 (误差 0.98) ✅ MAE 降至 2.11:意味着模型平均只猜错 2100 美元左右。对于波士顿房价数据集,这是一个非常优秀的深度学习成绩(通常 MAE 在 2-3 之间都算很好)。