第3章 深度神经网络原理及实现

本章学习目标

通过本章学习,你将能够:

  • ✅ 理解卷积神经网络(CNN)的原理和结构
  • ✅ 掌握循环神经网络(RNN)的工作原理
  • ✅ 了解生成对抗网络(GAN)的基本思想
  • ✅ 使用TensorFlow构建三种不同类型的神经网络
  • ✅ 完成三个实战项目:图像分类、文本情感分析、动漫人脸生成

第一部分:卷积神经网络(CNN)

3.1 卷积神经网络基础

3.1.1 什么是卷积神经网络?

【生活中的例子】

想象你在看一张照片,你是如何认出这是一只猫的?

1
2
3
4
5
6
7
你的眼睛会这样看:
第1步:看到线条、边缘、颜色块
第2步:看到眼睛、鼻子、耳朵的局部形状
第3步:把这些局部组合成完整的面部
第4步:认出"这是一只猫"

卷积神经网络(CNN)就是模仿这个过程!

【CNN的核心思想】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌─────────────────────────────────────────────────────────────┐
│                    传统神经网络 vs CNN                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  传统神经网络:                                               │
│  把图片拉直成一维 → 丢失了空间信息 → 参数量巨大                    │
│  例:28×28的图片变成784个像素 → 全部连接 → 参数爆炸               │
│                                                             │
│  CNN:                                                      │
│  保留空间结构 → 局部连接 → 权值共享 → 参数少,效果好               │
│  例:用3×3的卷积核扫描图片 → 共享9个参数 → 参数减少99%             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.1.2 CNN的三大核心操作

1. 卷积操作

【图解】卷积操作

卷积操作.webp
卷积操作

如上图所示,卷积操作其实就是每次取一个特定大小的矩阵 $F$(蓝色矩阵中的阴影部分),然后将其对输入$X$(图中蓝色矩阵)依次扫描并进行内积的运算过程。可以看到,阴影部分每移动一个位置就会计算得到一个卷积值(绿色矩阵中的阴影部分),当$F$扫描完成后就得到了整个卷积后的结果 $Y$ (绿色矩阵)。

【图解:卷积核如何工作】

卷积核如何工作.png
卷积核如何工作

(1)什么是卷积操作?

卷积操作是CNN的基础,可以通俗地理解为一种**“滑动加权求和”**的过程:

  • 把一个小模板(称为卷积核滤波器)在图像上从左到右、从上到下地滑动
  • 在每一个位置上,将模板上的数值与它覆盖住的图像区域的数值对应相乘
  • 然后将所有乘积相加,得到一个输出值
  • 所有这些输出值组成一张新的“特征图”

(2)卷积操作的目的

目的 说明
提取局部特征 卷积核相当于“特征探测器”,不同的卷积核可以检测不同的特征(边缘、纹理、形状等)
参数共享 同一个卷积核在整个图像上滑动时使用相同参数,大幅减少参数量,实现平移不变性
局部连接 每个输出单元只与输入的一小部分区域相连,符合图像局部相关性先验
多层级抽象 通过堆叠多层卷积,从简单特征逐步构建复杂特征

(3)卷积操作示例

假设有一个图像区域和卷积核:

1
2
3
4
5
6
图像区域:         卷积核:
[1, 0, 1]        [1, 0, 1]
[0, 1, 0]        [0, 1, 0]
[1, 0, 1]        [1, 0, 1]

卷积结果 = 1×1 + 0×0 + 1×1 + 0×0 + 1×1 + 0×0 + 1×1 + 0×0 + 1×1 = 4
2. 局部视野

(1)什么是局部视野?

在卷积操作中,输出中的每一个像素只由输入中一个局部区域(称为感受野)决定,这个局部区域的大小就是卷积核的尺寸(如3×3、5×5)。

(2)为什么要设计成局部视野?

原因 说明
符合图像统计特性 邻近像素高度相关,远处像素关联较弱
大幅减少参数 3×3卷积核仅9个参数,而全连接层参数过亿
强制学习局部模式 从边缘、纹理等小尺度模式逐步学习到整体形状

(3)局部视野如何变大?

虽然单层卷积只有很小的局部视野,但通过堆叠多层,高层的神经元可以“看到”更大的区域:

1
2
3
4
5
6
7
输入层
卷积层1(3×3) → 感受野大小 = 3×3
卷积层2(3×3) → 感受野大小 = 5×5
卷积层3(3×3) → 感受野大小 = 7×7

【代码实现:可视化各种卷积核的效果】

使用图片:cat.png

cat
  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from PIL import Image
import os

# 配置 matplotlib 以支持中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False


# 读取图片
def load_image(image_path, size=(224, 224)):
    """加载并预处理图片"""
    img = Image.open(image_path)
    img = img.convert('L')  # 转换为灰度图
    img = img.resize(size)  # 调整大小
    img_array = np.array(img, dtype=np.float32) / 255.0  # 归一化到[0, 1]
    return img_array


# 使用 cat.png 图片
image_path = 'cat.png'

# 检查图片是否存在
if not os.path.exists(image_path):
    print(f"错误: 找不到图片 {image_path}")
    print("请确保 cat.png 文件在当前目录下")
    exit()

# 加载图片
image = load_image(image_path, size=(224, 224))
print(f"图片形状: {image.shape}")
print(f"图片值范围: [{image.min():.3f}, {image.max():.3f}]")

# 创建不同的卷积核
sobel_x = tf.constant([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=tf.float32)
sobel_y = tf.constant([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=tf.float32)
blur = tf.constant([[1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9]], dtype=tf.float32)
sharpen = tf.constant([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=tf.float32)

# 转换为4D张量 [batch, height, width, channels]
image_4d = tf.reshape(image, [1, image.shape[0], image.shape[1], 1])

# 应用不同的卷积核
kernels = {
    'Sobel X (垂直边缘)': sobel_x,
    'Sobel Y (水平边缘)': sobel_y,
    '模糊效果': blur,
    '锐化效果': sharpen
}

results = {}
for name, kernel in kernels.items():
    kernel_4d = tf.reshape(kernel, [3, 3, 1, 1])
    result = tf.nn.conv2d(image_4d, kernel_4d, strides=[1, 1, 1, 1], padding='SAME')
    results[name] = tf.squeeze(result).numpy()

# 可视化结果
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
fig.suptitle('卷积核效果对比 - 猫咪图片', fontsize=16, fontweight='bold')

# 原始图片
axes[0, 0].imshow(image, cmap='gray')
axes[0, 0].set_title('原始图片', fontsize=12)
axes[0, 0].axis('off')

# Sobel X
axes[0, 1].imshow(results['Sobel X (垂直边缘)'], cmap='gray')
axes[0, 1].set_title('Sobel X (垂直边缘)', fontsize=12)
axes[0, 1].axis('off')

# Sobel Y
axes[0, 2].imshow(results['Sobel Y (水平边缘)'], cmap='gray')
axes[0, 2].set_title('Sobel Y (水平边缘)', fontsize=12)
axes[0, 2].axis('off')

# 模糊效果
axes[1, 0].imshow(results['模糊效果'], cmap='gray')
axes[1, 0].set_title('模糊效果', fontsize=12)
axes[1, 0].axis('off')

# 锐化效果
axes[1, 1].imshow(results['锐化效果'], cmap='gray')
axes[1, 1].set_title('锐化效果', fontsize=12)
axes[1, 1].axis('off')

# 边缘强度图(Sobel X 和 Sobel Y 的组合)
edge_magnitude = np.sqrt(results['Sobel X (垂直边缘)'] ** 2 + results['Sobel Y (水平边缘)'] ** 2)
axes[1, 2].imshow(edge_magnitude, cmap='gray')
axes[1, 2].set_title('边缘强度 (Sobel组合)', fontsize=12)
axes[1, 2].axis('off')

plt.tight_layout()
plt.show()

# 打印统计信息
print("\n=== 处理结果统计 ===")
for name, result in results.items():
    print(f"{name}:")
    print(f"  值范围: [{result.min():.4f}, {result.max():.4f}]")
    print(f"  均值: {result.mean():.4f}")
    print(f"  标准差: {result.std():.4f}")

代码功能说明

  • load_image():加载图片并转换为灰度图,调整大小并归一化到[0,1]范围
  • 创建四种不同的卷积核:Sobel X(垂直边缘检测)、Sobel Y(水平边缘检测)、模糊核、锐化核
  • 使用tf.nn.conv2d()对图片进行卷积操作,padding='SAME'保持输出尺寸与输入相同
  • 可视化原始图片和各卷积核处理后的效果,并统计输出结果的范围、均值、标准差

3. 池化操作

池化是卷积神经网络中紧随卷积层之后的关键操作,其核心思想是对局部区域进行下采样,用一个小区域的统计特征来代替该区域。

(1)池化的基本形式

类型 说明 示例
最大池化 取局部区域中的最大值作为输出 输入 [1,3;2,4] → 输出 4
平均池化 取局部区域中的平均值作为输出 输入 [1,3;2,4] → 输出 2.5

通常使用 2×2 池化窗口,步长为 2,特征图尺寸减半。

(2)池化的五大核心作用

作用 说明
降低空间维度 减少计算量,2×2池化后像素点减少到原来的1/4
增强平移不变性 对微小位移不敏感,特征在窗口内移动不影响输出
扩大感受野 降低分辨率间接扩大后续层的感受野
提取主要特征 最大池化保留最强激活,抑制噪声
防止过拟合 减少参数,强制学习更抽象的特征

(3)池化操作示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
输入特征图 (4×4)                    输出特征图 (2×2)
┌─────┬─────┬─────┬─────┐          ┌─────┬─────┐
│  1  │  2  │  3  │  4  │          │  6  │  8  │
├─────┼─────┼─────┼─────┤          ├─────┼─────┤
│  5  │  6  │  7  │  8  │   →      │ 14  │ 16  │
├─────┼─────┼─────┼─────┤          └─────┴─────┘
│  9  │ 10  │ 11  │ 12  │          取每个2×2区域的最大值
├─────┼─────┼─────┼─────┤          步长=2,不重叠
│ 13  │ 14  │ 15  │ 16  │          
└─────┴─────┴─────┴─────┘          

【代码实现:池化层】

 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
import tensorflow as tf
from PIL import Image
import numpy as np

# 1. 加载图片并转为张量
img = Image.open("cat.png")
img_array = np.array(img) / 255.0
img_tensor = tf.convert_to_tensor(img_array, dtype=tf.float32)
img_tensor = tf.reshape(img_tensor, [1, *img_tensor.shape])

# 2. 定义最大池化层
max_pool = tf.keras.layers.MaxPooling2D(
    pool_size=(2, 2),
    strides=(2, 2),
    padding='valid'
)

# 3. 执行池化
pooled_tensor = max_pool(img_tensor)

# 4. 转回图片格式并保存
pooled_img = tf.squeeze(pooled_tensor)
pooled_img = (pooled_img.numpy() * 255).astype(np.uint8)
pooled_img = Image.fromarray(pooled_img)
pooled_img.save("cat_pooled.png")

print(f"原始尺寸: {img.size}")
print(f"池化后尺寸: {pooled_img.size}")

代码功能说明

  • 加载图片并归一化到[0,1]范围,转换为TensorFlow张量格式
  • 创建MaxPooling2D层,池化窗口为2×2,步长为2,padding='valid'表示不填充
  • 对图片执行最大池化操作,每个2×2区域取最大值,输出尺寸减半
  • 将池化后的张量转回图片格式并保存为文件,打印原始尺寸和池化后尺寸对比

4. 全连接层
1
2
3
4
5
经过卷积和池化后 → 特征图被展平 → 全连接层 → 分类结果

例如:经过CNN处理后得到 64个 7×7 的特征图
展平后:64 × 7 × 7 = 3136 个特征
全连接层:3136 → 128 → 64 → 10(10个类别)

3.1.3 CNN的整体结构

卷积神经网络.png
卷积神经网络

 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
┌─────────────────────────────────────────────────────────────────────────┐
│                         CNN 完整结构示意图                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  输入图片               卷积+池化              卷积+池化                    │
│  28×28×1               26×26×32              12×12×64                   │
│                                                                         │
│  ┌──────┐             ┌────────┐            ┌────────┐                  │
│  │      │   Conv2D    │        │  MaxPool   │        │                  │
│  │ 图片  │ ─────────→  │ 特征图  │ ────────→  │ 特征图  │                  │
│  │      │  32个核      │  32层  │  2×2       │  32层  │                  │
│  └──────┘             └────────┘            └────────┘                  │
│      │                    │                    │                        │
│      ▼                    ▼                    ▼                        │
│                                                                         │
│      再卷积+池化            展平                 全连接                   │
│      8×8×64              4096个特征            10个类别                  │
│                                                                         │
│      ┌────────┐         ┌────────┐           ┌────────┐                │
│      │        │         │        │           │ Dense  │                │
│      │ 特征图  │ ──────→ │ 向量   │ ────────→  │  10    │ ──→ 猫/狗/车   │
│      │  64层  │  Flatten │ 4096  │           │ Softmax│                │
│      └────────┘         └────────┘           └────────┘                │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.2 CNN实战:Fashion-MNIST服饰图像分类

3.2.1 项目背景

Fashion-MNIST是一个服饰分类数据集,包含10个类别的服饰图片:

  • 0: T恤/上衣
  • 1: 裤子
  • 2: 套头衫
  • 3: 连衣裙
  • 4: 外套
  • 5: 凉鞋
  • 6: 衬衫
  • 7: 运动鞋
  • 8: 包
  • 9: 短靴

【项目目标】 使用CNN对服饰图片进行分类,准确率达到90%以上。

3.2.2 数据下载与保存

首先需要从TensorFlow内置数据集中下载Fashion-MNIST数据并保存到本地,以便后续重复使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import tensorflow as tf
import numpy as np
import os

# 创建本地数据目录
data_dir = "fashion_mnist_data"
os.makedirs(data_dir, exist_ok=True)

# 下载数据集
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

# 保存为 numpy 格式到本地
np.save(os.path.join(data_dir, "x_train.npy"), x_train)
np.save(os.path.join(data_dir, "y_train.npy"), y_train)
np.save(os.path.join(data_dir, "x_test.npy"), x_test)
np.save(os.path.join(data_dir, "y_test.npy"), y_test)

print("✅ Fashion-MNIST 数据已保存到本地目录:", data_dir)
print(f"训练集样本数: {x_train.shape[0]}")
print(f"测试集样本数: {x_test.shape[0]}")

代码功能说明

  • os.makedirs(data_dir, exist_ok=True):创建本地数据目录,如果目录已存在则不报错
  • tf.keras.datasets.fashion_mnist.load_data():从TensorFlow内置数据集下载Fashion-MNIST数据
  • np.save():将训练集图片(x_train)、训练集标签(y_train)、测试集图片(x_test)、测试集标签(y_test)分别保存为NumPy二进制文件
  • 保存为.npy格式的好处:避免重复下载,数据加载速度快,可直接用np.load()读取

【注】如果无法使用上面的代码下载数据集,则点击: fashion_mnist_data.zip 下载压缩包。

3.2.3 查看训练集和测试集

数据下载保存后,可以通过以下代码查看数据集的基本信息和样本内容。

 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# -*- coding: utf-8 -*-
"""
独立脚本:查看本地保存的 Fashion-MNIST 训练集 & 测试集
"""

import numpy as np
import matplotlib.pyplot as plt
import os

# ===================== 配置 =====================
data_dir = "fashion_mnist_data"  # 本地数据文件夹
class_names = ['T恤/上衣', '裤子', '套头衫', '连衣裙', '外套',
               '凉鞋', '衬衫', '运动鞋', '包', '短靴']

# 中文字体设置
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# ===================== 加载本地数据 =====================
print("正在从本地加载数据...")

x_train = np.load(os.path.join(data_dir, "x_train.npy"))
y_train = np.load(os.path.join(data_dir, "y_train.npy"))
x_test = np.load(os.path.join(data_dir, "x_test.npy"))
y_test = np.load(os.path.join(data_dir, "y_test.npy"))

# ===================== 打印数据信息 =====================
print("\n===== 数据集信息 =====")
print(f"训练集图片:{x_train.shape}   (数量:{x_train.shape[0]} 张,尺寸:{x_train.shape[1]}×{x_train.shape[2]})")
print(f"训练集标签:{y_train.shape}")
print(f"测试集图片:{x_test.shape}   (数量:{x_test.shape[0]} 张,尺寸:{x_test.shape[1]}×{x_test.shape[2]})")
print(f"测试集标签:{y_test.shape}")

print(f"\n像素值范围:{x_train.min()} ~ {x_train.max()}")
print(f"标签类别:0~9 共10类")

# ===================== 显示训练集前9张图 =====================
print("\n===== 显示训练集前9张图片 =====")
plt.figure(figsize=(10, 6))
for i in range(9):
    plt.subplot(3, 3, i+1)
    plt.imshow(x_train[i], cmap="gray")
    plt.title(f"标签:{y_train[i]}\n{class_names[y_train[i]]}")
    plt.axis("off")
plt.suptitle("训练集前9张图片", fontsize=14)
plt.tight_layout()
plt.show()

# ===================== 显示测试集前9张图 =====================
print("\n===== 显示测试集前9张图片 =====")
plt.figure(figsize=(10, 6))
for i in range(9):
    plt.subplot(3, 3, i+1)
    plt.imshow(x_test[i], cmap="gray")
    plt.title(f"标签:{y_test[i]}\n{class_names[y_test[i]]}")
    plt.axis("off")
plt.suptitle("测试集前9张图片", fontsize=14)
plt.tight_layout()
plt.show()

print("\n✅ 数据查看完成!")

代码功能说明

  • 数据加载:使用np.load()从本地加载之前保存的.npy文件
  • 数据信息打印:输出训练集和测试集的形状、样本数量、图片尺寸、像素值范围、标签类别数
  • 训练集可视化:显示训练集前9张图片,每张图片下方显示标签编号和类别名称
  • 测试集可视化:同样显示测试集前9张图片,便于对比训练集和测试集的样本分布
  • 中文字体配置:设置matplotlib支持中文显示,确保标题和标签正常显示

3.2.4 完整项目代码

  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""
项目三-1:Fashion-MNIST服饰图像分类
功能:使用卷积神经网络对10类服饰图片进行分类
"""

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
import seaborn as sns
from collections import Counter

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

EPOCHS = 15
BATCH_SIZE = 64
CLASS_NAMES = ['T恤/上衣', '裤子', '套头衫', '连衣裙', '外套',
               '凉鞋', '衬衫', '运动鞋', '包', '短靴']
DATA_DIR = "fashion_mnist_data"

print("=" * 60)
print("项目:Fashion-MNIST服饰图像分类(本地数据版)")
print("=" * 60)

print("\n【第1步】加载本地Fashion-MNIST数据集")
print("-" * 40)

required_files = ["x_train.npy", "y_train.npy", "x_test.npy", "y_test.npy"]
for file in required_files:
    if not os.path.exists(os.path.join(DATA_DIR, file)):
        raise FileNotFoundError(f"未找到 {file},请先运行数据下载脚本!")

x_train = np.load(os.path.join(DATA_DIR, "x_train.npy"))
y_train = np.load(os.path.join(DATA_DIR, "y_train.npy"))
x_test = np.load(os.path.join(DATA_DIR, "x_test.npy"))
y_test = np.load(os.path.join(DATA_DIR, "y_test.npy"))

print(f"训练集图片形状: {x_train.shape}")
print(f"训练集标签形状: {y_train.shape}")
print(f"测试集图片形状: {x_test.shape}")
print(f"测试集标签形状: {y_test.shape}")

plt.figure(figsize=(12, 8))
for i in range(9):
    plt.subplot(3, 3, i + 1)
    plt.imshow(x_train[i], cmap='gray')
    plt.title(f"{CLASS_NAMES[y_train[i]]}")
    plt.axis('off')
plt.suptitle('Fashion-MNIST训练样本示例')
plt.tight_layout()
plt.show()

print("\n【第2步】数据预处理")
print("-" * 40)

x_train = x_train / 255.0
x_test = x_test / 255.0

x_train = np.expand_dims(x_train, axis=-1)
x_test = np.expand_dims(x_test, axis=-1)

print(f"预处理后训练集形状: {x_train.shape}")
print(f"预处理后测试集形状: {x_test.shape}")
print(f"像素值范围: [{x_train.min():.1f}, {x_train.max():.1f}]")

print("\n【第3步】构建CNN模型")
print("-" * 40)

model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(28, 28, 1)),
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(10, activation='softmax')
])

model.summary()
total_params = model.count_params()
print(f"\n总参数量: {total_params:,}")

print("\n【第4步】编译模型")
print("-" * 40)

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

print("优化器: Adam")
print("损失函数: 稀疏分类交叉熵")
print("评估指标: 准确率")

print("\n【第5步】训练模型")
print("-" * 40)

history = model.fit(
    x_train, y_train,
    batch_size=BATCH_SIZE,
    epochs=EPOCHS,
    validation_split=0.2,
    verbose=1
)

print("\n训练完成!")

print("\n【第6步】评估模型")
print("-" * 40)

test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
print(f"测试集损失: {test_loss:.4f}")
print(f"测试集准确率: {test_acc:.4f}")
print(f"测试集准确率: {test_acc * 100:.2f}%")

print("\n【第7步】可视化训练过程")
print("-" * 40)

plt.figure(figsize=(14, 5))

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='训练损失', marker='o')
plt.plot(history.history['val_loss'], label='验证损失', marker='s')
plt.xlabel('训练轮数')
plt.ylabel('损失')
plt.title('损失曲线')
plt.legend()
plt.grid(True)

plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], label='训练准确率', marker='o')
plt.plot(history.history['val_accuracy'], label='验证准确率', marker='s')
plt.xlabel('训练轮数')
plt.ylabel('准确率')
plt.title('准确率曲线')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

print("\n【第8步】预测测试样本")
print("-" * 40)

num_images = 9
random_indices = np.random.choice(len(x_test), num_images, replace=False)

plt.figure(figsize=(12, 8))
for i, idx in enumerate(random_indices):
    plt.subplot(3, 3, i + 1)
    plt.imshow(x_test[idx].reshape(28, 28), cmap='gray')

    img = np.expand_dims(x_test[idx], axis=0)
    pred = model.predict(img, verbose=0)
    pred_class = np.argmax(pred)
    confidence = np.max(pred)

    is_correct = pred_class == y_test[idx]
    color = 'green' if is_correct else 'red'
    title = f"真实: {CLASS_NAMES[y_test[idx]]}\n预测: {CLASS_NAMES[pred_class]}\n置信度: {confidence:.2f}"
    plt.title(title, color=color, fontsize=9)
    plt.axis('off')

plt.suptitle('CNN模型预测结果(绿色正确,红色错误)', fontsize=14)
plt.tight_layout()
plt.show()

print("\n【第9步】分析错误样本")
print("-" * 40)

predictions = model.predict(x_test, verbose=0)
predicted_classes = np.argmax(predictions, axis=1)

errors = np.where(predicted_classes != y_test)[0]
print(f"测试集总样本数: {len(y_test)}")
print(f"预测错误的样本数: {len(errors)}")
print(f"错误率: {len(errors) / len(y_test) * 100:.2f}%")

cm = confusion_matrix(y_test, predicted_classes)
plt.figure(figsize=(12, 10))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES)
plt.xlabel('预测类别')
plt.ylabel('真实类别')
plt.title('混淆矩阵')
plt.tight_layout()
plt.show()

error_pairs = [(y_test[idx], predicted_classes[idx]) for idx in errors]
common_errors = Counter(error_pairs).most_common(5)
print("\n最常见的错误分类:")
for (true_class, pred_class), count in common_errors:
    print(f"  真实: {CLASS_NAMES[true_class]} → 预测: {CLASS_NAMES[pred_class]}, 数量: {count}")

print("\n【第10步】保存模型")
print("-" * 40)

save_dir = 'tmp/models'
os.makedirs(save_dir, exist_ok=True)

model_path = os.path.join(save_dir, 'fashion_mnist_cnn.keras')
model.save(model_path)

weights_path = os.path.join(save_dir, 'fashion_mnist_cnn_weights.weights.h5')
model.save_weights(weights_path)

print(f"模型已保存为: {model_path}")
print(f"模型权重已保存为: {weights_path}")

print("\n" + "=" * 60)
print("Fashion-MNIST服饰分类项目完成!")
print("=" * 60)

代码解释

 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
42
43
44
45
"""
项目三-1:Fashion-MNIST服饰图像分类
功能:使用卷积神经网络对10类服饰图片进行分类
"""

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
import seaborn as sns
from collections import Counter

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

EPOCHS = 15
BATCH_SIZE = 64
CLASS_NAMES = ['T恤/上衣', '裤子', '套头衫', '连衣裙', '外套',
               '凉鞋', '衬衫', '运动鞋', '包', '短靴']
DATA_DIR = "fashion_mnist_data"

print("=" * 60)
print("项目:Fashion-MNIST服饰图像分类(本地数据版)")
print("=" * 60)

print("\n【第1步】加载本地Fashion-MNIST数据集")
print("-" * 40)

required_files = ["x_train.npy", "y_train.npy", "x_test.npy", "y_test.npy"]
for file in required_files:
    if not os.path.exists(os.path.join(DATA_DIR, file)):
        raise FileNotFoundError(f"未找到 {file},请先运行数据下载脚本!")

x_train = np.load(os.path.join(DATA_DIR, "x_train.npy"))
y_train = np.load(os.path.join(DATA_DIR, "y_train.npy"))
x_test = np.load(os.path.join(DATA_DIR, "x_test.npy"))
y_test = np.load(os.path.join(DATA_DIR, "y_test.npy"))

print(f"训练集图片形状: {x_train.shape}")
print(f"训练集标签形状: {y_train.shape}")
print(f"测试集图片形状: {x_test.shape}")
print(f"测试集标签形状: {y_test.shape}")

代码功能说明

  • 设置环境变量TF_CPP_MIN_LOG_LEVEL为2,减少TensorFlow的冗余日志输出
  • 配置matplotlib支持中文显示
  • 定义训练轮数(EPOCHS=15)、批次大小(BATCH_SIZE=64)和类别名称
  • 检查本地数据文件是否存在,若缺失则抛出异常
  • 使用np.load()加载训练集和测试集的图片数据(x_train, x_test)和标签数据(y_train, y_test)
  • 打印数据集形状信息,确认数据加载成功
1
2
3
4
5
6
7
8
9
plt.figure(figsize=(12, 8))
for i in range(9):
    plt.subplot(3, 3, i + 1)
    plt.imshow(x_train[i], cmap='gray')
    plt.title(f"{CLASS_NAMES[y_train[i]]}")
    plt.axis('off')
plt.suptitle('Fashion-MNIST训练样本示例')
plt.tight_layout()
plt.show()

代码功能说明

  • 创建一个12×8英寸的图形窗口
  • 循环显示训练集前9张图片,每张图片使用灰度色彩映射(cmap=‘gray’)
  • 图片标题使用对应的类别名称
  • 关闭坐标轴显示,使图片展示更清晰
  • 使用tight_layout()自动调整子图间距
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print("\n【第2步】数据预处理")
print("-" * 40)

x_train = x_train / 255.0
x_test = x_test / 255.0

x_train = np.expand_dims(x_train, axis=-1)
x_test = np.expand_dims(x_test, axis=-1)

print(f"预处理后训练集形状: {x_train.shape}")
print(f"预处理后测试集形状: {x_test.shape}")
print(f"像素值范围: [{x_train.min():.1f}, {x_train.max():.1f}]")

代码功能说明

  • 将像素值除以255,归一化到[0,1]范围,有助于模型更快收敛
  • 使用np.expand_dims()在最后一个维度添加通道维度,将形状从(样本数, 28, 28)变为(样本数, 28, 28, 1)
  • 添加通道维度是因为Conv2D层要求输入格式为(batch, height, width, channels)
  • 打印预处理后的数据形状和像素值范围,确认归一化效果
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
print("\n【第3步】构建CNN模型")
print("-" * 40)

model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(28, 28, 1)),
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(10, activation='softmax')
])

model.summary()
total_params = model.count_params()
print(f"\n总参数量: {total_params:,}")

代码功能说明

  • 使用Sequential顺序模型构建CNN网络
  • 输入层:指定输入形状(28,28,1),对应28×28灰度图像
  • 第一卷积层:32个3×3卷积核,ReLU激活函数,提取基础特征(边缘、纹理)
  • 第一池化层:2×2最大池化,步长2,特征图尺寸减半
  • 第二卷积层:64个3×3卷积核,提取更复杂特征
  • 第二池化层:2×2最大池化,进一步降维
  • 第三卷积层:64个3×3卷积核,提取高级语义特征
  • 展平层:将二维特征图转换为一维向量,连接卷积层和全连接层
  • 全连接层:64个神经元,ReLU激活,整合提取的特征
  • Dropout层:随机丢弃50%的神经元,防止过拟合
  • 输出层:10个神经元,Softmax激活,输出10个类别的概率分布
  • 打印模型结构摘要和总参数量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print("\n【第4步】编译模型")
print("-" * 40)

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

print("优化器: Adam")
print("损失函数: 稀疏分类交叉熵")
print("评估指标: 准确率")

代码功能说明

  • optimizer=‘adam’:使用Adam优化器,自适应学习率,适合大多数场景
  • loss=‘sparse_categorical_crossentropy’:使用稀疏分类交叉熵损失函数,适用于整数标签(0-9)的多分类问题
  • metrics=[‘accuracy’]:使用准确率作为评估指标
  • 打印编译配置信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print("\n【第5步】训练模型")
print("-" * 40)

history = model.fit(
    x_train, y_train,
    batch_size=BATCH_SIZE,
    epochs=EPOCHS,
    validation_split=0.2,
    verbose=1
)

print("\n训练完成!")

代码功能说明

  • 使用model.fit()开始模型训练
  • batch_size=64:每批次处理64张图片
  • epochs=15:完整训练15轮
  • validation_split=0.2:从训练集中抽取20%作为验证集,用于监控模型泛化能力
  • verbose=1:显示训练进度条和日志
  • 训练过程中自动记录损失值和准确率,存储在history对象中
1
2
3
4
5
6
7
print("\n【第6步】评估模型")
print("-" * 40)

test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
print(f"测试集损失: {test_loss:.4f}")
print(f"测试集准确率: {test_acc:.4f}")
print(f"测试集准确率: {test_acc * 100:.2f}%")

代码功能说明

  • 使用model.evaluate()在测试集上评估模型性能
  • verbose=0:不输出详细日志
  • 返回测试集损失值和准确率
  • 打印测试集损失和准确率,确认模型在未见过数据上的表现
 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
print("\n【第7步】可视化训练过程")
print("-" * 40)

plt.figure(figsize=(14, 5))

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='训练损失', marker='o')
plt.plot(history.history['val_loss'], label='验证损失', marker='s')
plt.xlabel('训练轮数')
plt.ylabel('损失')
plt.title('损失曲线')
plt.legend()
plt.grid(True)

plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], label='训练准确率', marker='o')
plt.plot(history.history['val_accuracy'], label='验证准确率', marker='s')
plt.xlabel('训练轮数')
plt.ylabel('准确率')
plt.title('准确率曲线')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

代码功能说明

  • 创建14×5英寸的图形窗口,包含两个子图
  • 左子图(损失曲线):绘制训练损失和验证损失随轮数的变化,使用圆形和方形标记
  • 右子图(准确率曲线):绘制训练准确率和验证准确率随轮数的变化
  • 添加标签、图例和网格线,便于观察趋势
  • 通过曲线可以判断模型是否过拟合(训练损失下降但验证损失上升)
 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
print("\n【第8步】预测测试样本")
print("-" * 40)

num_images = 9
random_indices = np.random.choice(len(x_test), num_images, replace=False)

plt.figure(figsize=(12, 8))
for i, idx in enumerate(random_indices):
    plt.subplot(3, 3, i + 1)
    plt.imshow(x_test[idx].reshape(28, 28), cmap='gray')

    img = np.expand_dims(x_test[idx], axis=0)
    pred = model.predict(img, verbose=0)
    pred_class = np.argmax(pred)
    confidence = np.max(pred)

    is_correct = pred_class == y_test[idx]
    color = 'green' if is_correct else 'red'
    title = f"真实: {CLASS_NAMES[y_test[idx]]}\n预测: {CLASS_NAMES[pred_class]}\n置信度: {confidence:.2f}"
    plt.title(title, color=color, fontsize=9)
    plt.axis('off')

plt.suptitle('CNN模型预测结果(绿色正确,红色错误)', fontsize=14)
plt.tight_layout()
plt.show()

代码功能说明

  • 从测试集中随机选择9张图片进行预测
  • 对每张图片添加批次维度后输入模型进行预测
  • 使用np.argmax()获取预测类别,np.max()获取置信度
  • 根据预测是否正确,标题显示绿色(正确)或红色(错误)
  • 标题显示真实标签、预测标签和置信度
  • 展示模型在实际样本上的预测效果
 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
print("\n【第9步】分析错误样本")
print("-" * 40)

predictions = model.predict(x_test, verbose=0)
predicted_classes = np.argmax(predictions, axis=1)

errors = np.where(predicted_classes != y_test)[0]
print(f"测试集总样本数: {len(y_test)}")
print(f"预测错误的样本数: {len(errors)}")
print(f"错误率: {len(errors) / len(y_test) * 100:.2f}%")

cm = confusion_matrix(y_test, predicted_classes)
plt.figure(figsize=(12, 10))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES)
plt.xlabel('预测类别')
plt.ylabel('真实类别')
plt.title('混淆矩阵')
plt.tight_layout()
plt.show()

error_pairs = [(y_test[idx], predicted_classes[idx]) for idx in errors]
common_errors = Counter(error_pairs).most_common(5)
print("\n最常见的错误分类:")
for (true_class, pred_class), count in common_errors:
    print(f"  真实: {CLASS_NAMES[true_class]} → 预测: {CLASS_NAMES[pred_class]}, 数量: {count}")

代码功能说明

  • 对全部测试集进行预测,获取预测类别
  • 找出预测错误的样本索引,计算错误数量和错误率
  • 使用confusion_matrix()计算混淆矩阵,展示每个类别的预测情况
  • 使用热力图可视化混淆矩阵,横轴为预测类别,纵轴为真实类别
  • 统计最常见的错误分类对,帮助分析模型的薄弱环节
  • 例如:如果衬衫经常被误判为T恤,说明这两类服饰特征相似
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
print("\n【第10步】保存模型")
print("-" * 40)

save_dir = 'tmp/models'
os.makedirs(save_dir, exist_ok=True)

model_path = os.path.join(save_dir, 'fashion_mnist_cnn.keras')
model.save(model_path)

weights_path = os.path.join(save_dir, 'fashion_mnist_cnn_weights.weights.h5')
model.save_weights(weights_path)

print(f"模型已保存为: {model_path}")
print(f"模型权重已保存为: {weights_path}")

print("\n" + "=" * 60)
print("Fashion-MNIST服饰分类项目完成!")
print("=" * 60)

代码功能说明

  • 创建保存目录tmp/models,如果目录不存在则创建
  • 使用model.save()保存完整的模型(包括网络结构、权重、优化器状态),文件格式为.keras
  • 使用model.save_weights()单独保存模型权重,文件格式为.weights.h5
  • 两种保存方式各有用途:完整模型可直接加载继续训练,权重文件可用于迁移学习
  • 打印保存路径,确认模型已成功保存

3.2.5 CNN模型结构总结

本项目采用的CNN模型结构如下:

 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
model = tf.keras.Sequential([
    # 1. 输入层:定义图像输入格式
    tf.keras.layers.Input(shape=(28, 28, 1)),
    
    # 2. 第一卷积+池化层:提取基础图像特征
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    
    # 3. 第二卷积+池化层:提取深层图像特征
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    
    # 4. 第三卷积层:进一步提炼高级特征
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    
    # 5. 展平层:将二维特征图转为一维向量
    tf.keras.layers.Flatten(),
    
    # 6. 全连接层:特征整合与分类决策
    tf.keras.layers.Dense(64, activation='relu'),
    
    # 7. Dropout层:防止模型过拟合
    tf.keras.layers.Dropout(0.5),
    
    # 8. 输出层:10分类概率输出
    tf.keras.layers.Dense(10, activation='softmax')
])

各层功能说明:

功能
输入层 固定输入形状(28,28,1),对应单通道灰度图
卷积层 提取图像特征(边缘、纹理、轮廓),32/64为卷积核数量,(3,3)为核大小
池化层 下采样降维,减少计算量,保留核心特征
展平层 将二维特征图转换为一维向量,衔接全连接层
全连接层 整合所有特征,完成从特征到分类逻辑的转换
Dropout层 随机丢弃50%神经元,防止过拟合
输出层 10个神经元对应10类服饰,softmax输出概率分布

3.3 CNN项目练习

【练习1:调整网络结构】

尝试修改CNN的网络结构,观察准确率的变化:

  1. 增加卷积层数量(从3层增加到4层)
  2. 增加卷积核数量(32→64→128)
  3. 添加更多的Dropout层
  4. 改变池化层的大小
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 练习代码框架
def create_cnn_model_v2():
    model = tf.keras.Sequential([
        # 你的代码:设计一个更深的CNN
        tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
        tf.keras.layers.MaxPooling2D((2, 2)),
        
        # 添加更多层...
        tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
        tf.keras.layers.MaxPooling2D((2, 2)),
        
        tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),
        tf.keras.layers.GlobalAveragePooling2D(),  # 全局平均池化代替Flatten
        
        tf.keras.layers.Dense(10, activation='softmax')
    ])
    return model

代码功能说明

  • 此练习框架展示了一个更深的CNN结构
  • 第三层卷积核数量从64增加到128,增强特征提取能力
  • 使用GlobalAveragePooling2D()替代Flatten()+Dense,大幅减少参数量
  • 尝试不同的网络结构,观察对准确率的影响

【练习2:数据增强】

使用数据增强技术来提高模型泛化能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 数据增强
data_augmentation = tf.keras.Sequential([
    tf.keras.layers.RandomRotation(0.1),      # 随机旋转
    tf.keras.layers.RandomZoom(0.1),          # 随机缩放
    tf.keras.layers.RandomTranslation(0.1, 0.1)  # 随机平移
])

# 在模型中添加数据增强层
model_with_aug = tf.keras.Sequential([
    data_augmentation,  # 第一层是数据增强
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    # ... 其余层不变
])

代码功能说明

  • RandomRotation(0.1):随机旋转图片,最大旋转角度为10%(0.1×360°=36°)
  • RandomZoom(0.1):随机缩放图片,缩放范围为[0.9, 1.1]
  • RandomTranslation(0.1, 0.1):随机平移图片,平移范围为宽高的10%
  • 数据增强在训练时实时生成变体图片,相当于扩充了训练集,提高模型泛化能力

【练习3:迁移学习】

使用预训练的VGG16模型进行特征提取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 加载预训练模型
base_model = tf.keras.applications.VGG16(
    weights='imagenet',
    include_top=False,
    input_shape=(32, 32, 3)
)

# 冻结预训练层
base_model.trainable = False

# 构建新模型
model = tf.keras.Sequential([
    tf.keras.layers.Resizing(32, 32),  # 调整图片大小
    tf.keras.layers.Conv2D(3, (3, 3), padding='same'),  # 转换为3通道
    base_model,
    tf.keras.layers.GlobalAveragePooling2D(),
    tf.keras.layers.Dense(10, activation='softmax')
])

代码功能说明

  • 加载在ImageNet数据集上预训练的VGG16模型,include_top=False表示不包含顶部分类层
  • base_model.trainable = False冻结预训练层,使其参数在训练过程中不更新
  • 使用Resizing层将输入图片调整为VGG16要求的尺寸(32,32)
  • 添加卷积层将单通道灰度图转换为3通道RGB图像
  • 在预训练模型后添加全局平均池化和分类层,完成服饰分类任务
  • 迁移学习能利用预训练模型已经学到的通用特征,在小数据集上获得更好效果

第二部分:循环神经网络(RNN)

3.4 循环神经网络基础

3.4.1 什么是循环神经网络?

【生活中的例子】

想象你在读一句话:“我今天吃了苹果,它很甜。”

当你读到"它"的时候,你知道"它"指的是"苹果"。为什么?因为你有记忆!你记住了前面提到的"苹果"。

传统神经网络没有这种记忆能力:

  • 它会把每个词单独处理,忘记前面看到的内容
  • 就像一个人有健忘症,读完一个词就忘掉上一个词

循环神经网络(RNN)就是为了解决这个问题而设计的:

  • RNN有一个循环结构,可以把信息传递给下一步
  • 就像人类阅读时,大脑会记住前面的内容来理解后面的内容

【RNN的核心思想】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────────┐
│                    传统神经网络 vs RNN                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  传统神经网络:                                               │
│  输入 → 网络 → 输出                                          │
│  每次处理独立,没有记忆                                       │
│                                                             │
│  RNN:                                                      │
│  输入 → 网络 → 输出 ──┐                                      │
│         ↑            │                                      │
│         └── 记忆 ────┘                                      │
│  每个时刻的输出依赖于当前输入和之前的记忆                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3.4.2 RNN的核心操作

1. 循环单元

【图解】RNN循环单元

1
2
3
4
5
6
7
8
                    ┌─────────────────┐
                    │                 │
   输入 x_t ──────→ │    RNN单元      │ ──────→ 输出 h_t
                    │                 │
         ┌───────── │                 │
         │          └─────────────────┘
         │                  ↑
         └── 上一时刻记忆 h_{t-1}

(1)什么是RNN循环单元?

RNN的核心是一个循环单元,它包含两个关键信息:

  • 当前输入 x_t:当前时刻的数据(如当前单词)
  • 隐藏状态 h_{t-1}:上一时刻的记忆

RNN单元会结合这两个信息,产生:

  • 当前输出 h_t:当前时刻的输出(也是传递给下一时刻的记忆)

(2)RNN的计算公式

1
2
3
4
5
6
7
8
9
h_t = tanh(W_h · h_{t-1} + W_x · x_t + b)

其中:
- h_t:当前时刻的隐藏状态(记忆)
- h_{t-1}:上一时刻的隐藏状态
- x_t:当前时刻的输入
- W_h、W_x:权重矩阵
- b:偏置项
- tanh:激活函数,将输出限制在[-1, 1]之间

【代码实现:RNN循环单元手动计算】

 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
42
43
44
45
46
47
48
49
50
import numpy as np

## 手动实现一个简单的RNN单元
def simple_rnn_cell(x_t, h_prev, W_x, W_h, b):
    """
    手动计算RNN单元的前向传播
    
    参数:
        x_t: 当前时刻输入,形状 (input_dim,)
        h_prev: 上一时刻隐藏状态,形状 (hidden_dim,)
        W_x: 输入权重矩阵,形状 (hidden_dim, input_dim)
        W_h: 隐藏状态权重矩阵,形状 (hidden_dim, hidden_dim)
        b: 偏置项,形状 (hidden_dim,)
    
    返回:
        h_t: 当前时刻隐藏状态
    """
    ## 计算公式: h_t = tanh(W_x @ x_t + W_h @ h_prev + b)
    h_t = np.tanh(np.dot(W_x, x_t) + np.dot(W_h, h_prev) + b)
    return h_t

## 设置参数
input_dim = 4   ## 输入维度(如词向量维度)
hidden_dim = 3  ## 隐藏状态维度

## 初始化权重和偏置
np.random.seed(42)
W_x = np.random.randn(hidden_dim, input_dim)  ## 3×4
W_h = np.random.randn(hidden_dim, hidden_dim) ## 3×3
b = np.random.randn(hidden_dim)              ## 3

## 模拟处理一个序列
x_1 = np.array([1, 0, 0, 1])   ## 第一个词
x_2 = np.array([0, 1, 1, 0])   ## 第二个词
x_3 = np.array([1, 1, 0, 0])   ## 第三个词

## 初始化隐藏状态(初始记忆为空)
h_prev = np.zeros(hidden_dim)

## 依次处理每个词
print("=== RNN循环单元处理过程 ===")
print(f"初始记忆: {h_prev}\n")

for t, x_t in enumerate([x_1, x_2, x_3], 1):
    h_t = simple_rnn_cell(x_t, h_prev, W_x, W_h, b)
    print(f"时刻 {t}:")
    print(f"  输入: {x_t}")
    print(f"  上一时刻记忆: {h_prev}")
    print(f"  当前记忆: {h_t}\n")
    h_prev = h_t  ## 将当前记忆传递给下一时刻

代码功能说明

  • simple_rnn_cell():手动实现单个RNN单元的前向传播,使用tanh激活函数
  • 随机初始化权重矩阵W_x(输入权重)和W_h(记忆权重)
  • 模拟处理三个时间步的序列,展示记忆如何从上一时刻传递到当前时刻
  • 每个时刻的输出都依赖于当前输入和上一时刻的记忆

2. 序列处理模式

RNN可以处理不同形式的序列数据:

模式 说明 示例
many-to-one 多输入 → 单输出 文本情感分类(一篇文章→一个评分)
one-to-many 单输入 → 多输出 图像描述(一张图→一段文字)
many-to-many 多输入 → 多输出(等长) 词性标注(每个词→一个标签)
many-to-many 多输入 → 多输出(不等长) 机器翻译(英文句子→中文句子)

【图解】序列处理模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
many-to-one (情感分析):           many-to-many (词性标注):
输入: 我 爱 这 部 电影            输入: 我   爱   这   部   电影
      ↓  ↓  ↓  ↓  ↓               ↓    ↓    ↓    ↓    ↓
RNN:  →  →  →  →  →              →    →    →    →    →
      ↓                           ↓    ↓    ↓    ↓    ↓
输出: 正面情感                   输出: 代词 动词 限定词 量词 名词

one-to-many (图像描述):           many-to-many (机器翻译):
输入: [图片特征]                  输入: I    love   this   movie
      ↓                           ↓     ↓     ↓      ↓
RNN:  →  →  →                    RNN:  →    →    →    →
      ↓  ↓  ↓                           ↓    ↓    ↓    ↓
输出: 一 只 猫                    输出: 我   爱    这   部   电影

【代码实现:三种RNN模式】

 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
import tensorflow as tf
import numpy as np

print("=== RNN三种工作模式 ===\n")

## 1. many-to-one:只返回最后一个输出
print("1. many-to-one 模式 (return_sequences=False)")
model_many_to_one = tf.keras.Sequential([
    tf.keras.layers.SimpleRNN(64, input_shape=(10, 32), return_sequences=False)
])
## 输入: (batch=32, 时间步=10, 特征=32)
sample_input = np.random.randn(32, 10, 32)
output = model_many_to_one(sample_input)
print(f"  输入形状: {sample_input.shape}")
print(f"  输出形状: {output.shape}  ## 只有最后一个时刻的输出\n")

## 2. many-to-many:返回所有时刻的输出
print("2. many-to-many 模式 (return_sequences=True)")
model_many_to_many = tf.keras.Sequential([
    tf.keras.layers.SimpleRNN(64, input_shape=(10, 32), return_sequences=True)
])
output = model_many_to_many(sample_input)
print(f"  输入形状: {sample_input.shape}")
print(f"  输出形状: {output.shape}  ## 每个时刻都有输出\n")

## 3. 堆叠RNN:多层RNN叠加
print("3. 堆叠RNN (多层)")
model_stacked = tf.keras.Sequential([
    ## 第一层返回所有输出给第二层
    tf.keras.layers.SimpleRNN(64, return_sequences=True, input_shape=(10, 32)),
    ## 第二层只返回最后一个输出
    tf.keras.layers.SimpleRNN(32, return_sequences=False)
])
output = model_stacked(sample_input)
print(f"  输入形状: {sample_input.shape}")
print(f"  输出形状: {output.shape}")

代码功能说明

  • many-to-one模式return_sequences=False,RNN只输出最后一个时间步的结果,适合情感分类等任务
  • many-to-many模式return_sequences=True,RNN输出每个时间步的结果,适合词性标注等任务
  • 堆叠RNN:多层RNN叠加,第一层return_sequences=True将全部输出传给第二层,增强模型表达能力

3. 梯度消失与LSTM/GRU

【问题】RNN的梯度消失

RNN在处理长序列时会遇到梯度消失问题:

  • 随着序列变长,早期信息在传递过程中逐渐被"遗忘"
  • 就像传话游戏,第一个人说一句话,传到最后一个人时意思完全变了
1
2
3
4
5
6
7
长序列记忆衰减:
时刻1: "我" → 记忆强度 100%
时刻2: "爱" → 记忆强度 60%
时刻3: "这" → 记忆强度 36%
时刻4: "部" → 记忆强度 21%
时刻5: "电" → 记忆强度 13%
时刻6: "影" → 记忆强度 8%  ← 几乎忘记了开头的"我"

【解决方案】LSTM和GRU

模型 特点 适用场景
LSTM 三个门控(遗忘门、输入门、输出门),记忆能力强 长文本、时间序列、语音识别
GRU 两个门控(更新门、重置门),结构更简单 中等长度序列、计算资源有限
SimpleRNN 无门控机制,只能处理短序列 极短序列、教学演示

【图解】LSTM的门控结构

1
2
3
4
5
6
7
8
                    ┌─────────────────────────────────┐
                    │            LSTM单元              │
                    │                                 │
   上一时刻输出 ────┼─→ 遗忘门 ──→ 决定忘记什么          │
   上一时刻记忆 ────┼─→ 输入门 ──→ 决定存储什么          │
   当前输入 ────────┼─→ 输出门 ──→ 决定输出什么          │
                    │                                 │
                    └─────────────────────────────────┘

【代码实现:三种RNN对比】

 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
42
43
44
45
46
47
48
49
50
51
52
import tensorflow as tf
import numpy as np

print("=== SimpleRNN、LSTM、GRU 对比 ===\n")

# 设置参数
vocab_size = 10000   # 词典大小
embedding_dim = 128  # 词向量维度
max_length = 100     # 序列最大长度
hidden_units = 64    # 隐藏单元数

# 1. SimpleRNN
print("1. SimpleRNN (基础循环神经网络)")
simple_rnn_model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length),
    tf.keras.layers.SimpleRNN(hidden_units, return_sequences=False),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
# 显式构建模型
simple_rnn_model.build(input_shape=(None, max_length))
simple_rnn_model.summary()

print("\n" + "-" * 50 + "\n")

# 2. LSTM
print("2. LSTM (长短期记忆网络) - 解决长距离依赖")
lstm_model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length),
    tf.keras.layers.LSTM(hidden_units, return_sequences=False),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
lstm_model.build(input_shape=(None, max_length))
lstm_model.summary()

print("\n" + "-" * 50 + "\n")

# 3. GRU
print("3. GRU (门控循环单元) - LSTM的简化版本")
gru_model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embedding_dim, input_length=max_length),
    tf.keras.layers.GRU(hidden_units, return_sequences=False),
    tf.keras.layers.Dense(1, activation='sigmoid')
])
gru_model.build(input_shape=(None, max_length))
gru_model.summary()

# 对比三种模型的参数量
print("\n" + "=" * 50)
print("模型参数量对比:")
print(f"  SimpleRNN: {simple_rnn_model.count_params():,} 参数")
print(f"  LSTM:      {lstm_model.count_params():,} 参数")
print(f"  GRU:       {gru_model.count_params():,} 参数")

代码功能说明

  • 分别构建SimpleRNN、LSTM、GRU三种模型进行对比
  • 每个模型都包含Embedding层(将单词转为向量)、RNN层(处理序列)、Dense层(输出分类)
  • return_sequences=False表示只取最后一个时间步的输出
  • 通过summary()查看各模型的参数量,LSTM参数量最多(因为有三个门控结构)

3.4.3 Embedding层

【什么是Embedding?】

计算机不认识单词,只认识数字。Embedding层的作用就是把单词转换成数字向量

1
2
3
4
5
6
7
单词 → Embedding → 向量表示

"苹果"  →  [0.2, -0.5, 0.8, 0.1, ...]
"香蕉"  →  [0.3, 0.1, -0.2, 0.7, ...]
"水果"  →  [0.25, -0.2, 0.3, 0.4, ...]

相似含义的单词会有相似的向量表示!

【Embedding层的工作原理】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────┐
│                     Embedding层示意图                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   输入(单词索引)           Embedding矩阵           输出(词向量)│
│                                                             │
│   "猫" → 索引 42     →    ┌──────────────┐      →   [0.1, 0.5, ...]│
│                           │ 第42行向量   │                     │
│   "狗" → 索引 58     →    │ 第58行向量   │      →   [0.3, 0.2, ...]│
│                           └──────────────┘                     │
│                                                             │
│   Embedding矩阵形状: (vocab_size, embedding_dim)              │
│   - vocab_size: 词典大小(如10000个单词)                       │
│   - embedding_dim: 每个单词的向量维度(如128维)                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

【代码实现:Embedding层示例】

 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
42
43
import tensorflow as tf
import numpy as np

print("=== Embedding层示例 ===\n")

## 创建Embedding层
vocab_size = 1000   ## 词典大小:1000个单词
embedding_dim = 64  ## 每个单词用64维向量表示
input_length = 10   ## 每个句子包含10个单词

embedding_layer = tf.keras.layers.Embedding(
    input_dim=vocab_size,
    output_dim=embedding_dim,
    input_length=input_length
)

## 方法1:构建层(推荐)
embedding_layer.build(input_shape=(None, input_length))

print(f"Embedding层参数矩阵形状: {embedding_layer.weights[0].shape}")
print(f"  - 行数: {vocab_size} (词典中的每个单词)")
print(f"  - 列数: {embedding_dim} (每个单词的向量维度)")

## 模拟输入:32个句子,每个句子10个单词,单词索引随机从0-999选取
input_array = np.random.randint(0, vocab_size, size=(32, input_length))
print(f"\n输入形状: {input_array.shape}  ## (batch_size, sequence_length)")

## 通过Embedding层
output_array = embedding_layer(input_array)
print(f"输出形状: {output_array.shape}  ## (batch_size, sequence_length, embedding_dim)")

## 查看具体的一个单词的向量
print(f"\n索引为42的单词对应的向量:")
word_vector = embedding_layer.weights[0][42].numpy()
print(f"  向量维度: {word_vector.shape}")
print(f"  向量值: [{word_vector[0]:.4f}, {word_vector[1]:.4f}, ..., {word_vector[-1]:.4f}]")

## 演示:相同含义的单词会有相似的向量
print("\n=== 训练后相似单词的向量会相近 ===")
print("Embedding层会在训练过程中学习单词的语义关系:")
print("  'king' 和 'queen' 的向量会比较接近")
print("  'apple' 和 'orange' 的向量会比较接近")
print("  'good' 和 'bad' 的向量会相距较远")

代码功能说明

  • Embedding层本质是一个可训练的查找表,形状为(vocab_size, embedding_dim)
  • 输入是单词索引(整数),输出是对应的词向量(浮点数数组)
  • 输入形状:(batch_size, sequence_length)
  • 输出形状:(batch_size, sequence_length, embedding_dim)
  • Embedding层的参数在训练过程中会不断更新,学习单词的语义关系

3.4.4 RNN的整体结构

 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
┌─────────────────────────────────────────────────────────────────────────┐
│                      RNN 完整结构示意图(文本情感分析)                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  输入句子                Embedding层              RNN层                  │
│  "I love this movie"                                                     │
│                                                                         │
│  索引序列               词向量序列              隐藏状态序列               │
│  [42, 156, 8, 523]     ┌──────┐              ┌──────┐                  │
│      ↓                 │[0.2, │              │[0.1, │                  │
│  Embedding层           │ 0.5, │    RNN       │ 0.3, │                  │
│      ↓                 │ ...] │    →         │ ...] │                  │
│  ┌──────────────┐      └──────┘              └──────┘                  │
│  │ 词向量序列   │         ↓                      ↓                      │
│  │ (10, 128)   │      ┌──────┐              ┌──────┐                  │
│  └──────────────┘      │[0.1, │              │[0.4, │                  │
│         ↓              │ 0.3, │    →         │ 0.2, │                  │
│     RNN层              │ ...] │              │ ...] │                  │
│         ↓              └──────┘              └──────┘                  │
│    最后一个输出           ...                    ↓                      │
│         ↓                                        最后一个隐藏状态        │
│    全连接层                                              ↓              │
│         ↓                                          全连接层             │
│    输出(正面/负面)                                          ↓           │
│                                                      输出(正面/负面)     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

3.5 RNN实战:IMDB文本情感分析

3.5.1 项目背景

【项目目标】

IMDB(互联网电影数据库)是一个知名的电影评论网站。本项目将使用循环神经网络(RNN)对IMDB的电影评论进行情感分类,判断评论是正面还是负面

1
2
3
示例评论:
"这部电影太棒了,演员表演精彩,剧情扣人心弦!" → 正面情感 ✅
"浪费时间,剧情无聊,演技尴尬,不推荐。"     → 负面情感 ❌

【项目价值】

  • 自动分析用户评论情感,帮助商家了解产品口碑
  • 监控社交媒体舆情,及时发现负面信息
  • 推荐系统可以根据用户喜好推荐内容

3.5.2 数据集介绍

【IMDB数据集】

IMDB数据集包含50,000条电影评论,分为25,000条训练数据和25,000条测试数据,正负面评论各占50%。

属性 说明
数据来源 IMDB电影评论网站
样本数量 50,000条(训练集25,000 + 测试集25,000)
分类标签 1 = 正面情感,0 = 负面情感
数据格式 每条评论已转换为单词索引序列
词典大小 88,584个单词(实际使用前10,000个)

【数据预处理流程】

1
2
3
原始评论文本                   单词索引序列                 固定长度序列
"this movie is great"  →  [12, 45, 78, 234]  →  [12, 45, 78, 234, 0, 0, ...]
                                                    (长度统一为256)

【数据查看代码】

 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
42
43
44
45
## 1_view_data.py - 查看训练集和测试集内容
import numpy as np
import json
from tensorflow.keras.preprocessing.sequence import pad_sequences

## ===================== 加载数据 =====================
X = np.load('../data/imdb.npz', allow_pickle=True)
train_x = X['x_train']
test_x = X['x_test']
train_y = X['y_train']
test_y = X['y_test']

## 加载词典
path = '../data/imdb_word_index.json'
with open(path, 'r', encoding='utf-8') as f:
    word_index = json.load(f)

word2id = {k: (v + 1) for k, v in word_index.items()}
word2id['<PAD>'] = 0
word2id['<UNK>'] = 1
id2word = {v: k for k, v in word2id.items()}

## 数字序列 → 句子
def get_words(sent_ids):
    return ' '.join([id2word.get(i + 1, '<UNK>') for i in sent_ids])

## ===================== 查看训练集前5条 =====================
print("=" * 60)
print("📌 训练集 train 前 5 条(标签 + 评论内容)")
print("=" * 60)
for i in range(5):
    label = train_y[i]
    sentiment = "正面(1)" if label == 1 else "负面(0)"
    print(f"\n【第 {i+1} 条】标签:{sentiment}")
    print("内容:", get_words(train_x[i])[:300], "...")

## ===================== 查看测试集前5条 =====================
print("\n" + "=" * 60)
print("📌 测试集 test 前 5 条(标签 + 评论内容)")
print("=" * 60)
for i in range(5):
    label = test_y[i]
    sentiment = "正面(1)" if label == 1 else "负面(0)"
    print(f"\n【第 {i+1} 条】标签:{sentiment}")
    print("内容:", get_words(test_x[i])[:300], "...")

代码功能说明

  • 加载IMDB数据集(已预处理为数字索引序列)和词典文件
  • 构建word2id映射(单词→索引)和id2word映射(索引→单词)
  • get_words()函数将数字索引序列还原为可读的英文句子
  • 分别展示训练集和测试集前5条评论的内容和情感标签

运行结果示例

1
2
3
4
5
6
============================================================
📌 训练集 train 前 5 条(标签 + 评论内容)
============================================================

【第 1 条】标签:正面(1)
内容: this film was just brilliant casting location scenery story direction everyone's ...

3.5.3 完整项目代码

第一部分:SimpleRNN模型训练
  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# part1_simple_rnn.py
# IMDB情感分析 - SimpleRNN模型

import tensorflow as tf
import numpy as np
import json
import matplotlib.pyplot as plt

# 配置matplotlib中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False

print("=" * 60)
print("IMDB情感分析 - SimpleRNN模型")
print("=" * 60)

# ===================== 1. 加载数据 =====================
print("\n【第1步】加载IMDB数据集")
print("-" * 40)

X = np.load('../data/imdb.npz', allow_pickle=True)
train_x = X['x_train']
test_x = X['x_test']
train_y = X['y_train']
test_y = X['y_test']

print(f"训练集样本数: {len(train_x)}")
print(f"测试集样本数: {len(test_x)}")
print(f"训练集标签分布: 正面={sum(train_y)}, 负面={len(train_y) - sum(train_y)}")

# ===================== 2. 加载词典 =====================
print("\n【第2步】加载词典")
print("-" * 40)

path = '../data/imdb_word_index.json'
with open(path, 'r', encoding='utf-8') as f:
    word_index = json.load(f)

# 构建单词到索引的映射
word2id = {k: (v + 3) for k, v in word_index.items()}
word2id['<PAD>'] = 0
word2id['<START>'] = 1
word2id['<UNK>'] = 2
word2id['<UNUSED>'] = 3

id2word = {v: k for k, v in word2id.items()}
vocab_size = len(word2id)
print(f"词典大小: {vocab_size}")

# ===================== 3. 数据预处理 =====================
print("\n【第3步】数据预处理")
print("-" * 40)

# 将索引调整(原数据索引从1开始,需要+3)
train_text = [[idx + 3 for idx in text] for text in train_x]
test_text = [[idx + 3 for idx in text] for text in test_x]

# 序列长度统一为256
maxlen = 256
train_data = tf.keras.preprocessing.sequence.pad_sequences(
    train_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)
test_data = tf.keras.preprocessing.sequence.pad_sequences(
    test_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)

print(f"训练数据形状: {train_data.shape}")
print(f"测试数据形状: {test_data.shape}")

# ===================== 4. 构建SimpleRNN模型 =====================
print("\n【第4步】构建SimpleRNN模型")
print("-" * 40)

def build_simple_rnn_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(
            input_dim=vocab_size,
            output_dim=128,
            input_length=maxlen
        ),
        tf.keras.layers.SimpleRNN(
            units=64,
            return_sequences=False,
            dropout=0.2
        ),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

simple_rnn_model = build_simple_rnn_model()
simple_rnn_model.summary()

# ===================== 5. 编译模型 =====================
print("\n【第5步】编译模型")
print("-" * 40)

simple_rnn_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
    loss=tf.keras.losses.BinaryCrossentropy(),
    metrics=['accuracy']
)

# ===================== 6. 训练模型 =====================
print("\n【第6步】训练SimpleRNN模型")
print("-" * 40)

EPOCHS = 5  # 增加到5轮以获得更好效果
BATCH_SIZE = 64

history_simple = simple_rnn_model.fit(
    train_data, train_y,
    batch_size=BATCH_SIZE,
    epochs=EPOCHS,
    validation_split=0.2,
    verbose=1
)

# ===================== 7. 评估模型 =====================
print("\n【第7步】评估SimpleRNN模型")
print("-" * 40)

test_loss, test_acc = simple_rnn_model.evaluate(test_data, test_y, verbose=0)
print(f"测试集损失: {test_loss:.4f}")
print(f"测试集准确率: {test_acc:.4f}")
print(f"SimpleRNN准确率: {test_acc * 100:.2f}%")

# 保存模型和训练历史
simple_rnn_model.save('simple_rnn_model.h5')
np.save('simple_rnn_history.npy', history_simple.history)

print("\nSimpleRNN模型训练完成!")
第二部分:LSTM模型训练
  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# part2_lstm.py
# IMDB情感分析 - LSTM模型

import tensorflow as tf
import numpy as np
import json
import matplotlib.pyplot as plt

# 配置matplotlib中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False

print("=" * 60)
print("IMDB情感分析 - LSTM模型")
print("=" * 60)

# ===================== 1. 加载数据 =====================
print("\n【第1步】加载IMDB数据集")
print("-" * 40)

X = np.load('../data/imdb.npz', allow_pickle=True)
train_x = X['x_train']
test_x = X['x_test']
train_y = X['y_train']
test_y = X['y_test']

print(f"训练集样本数: {len(train_x)}")
print(f"测试集样本数: {len(test_x)}")

# ===================== 2. 加载词典 =====================
print("\n【第2步】加载词典")
print("-" * 40)

path = '../data/imdb_word_index.json'
with open(path, 'r', encoding='utf-8') as f:
    word_index = json.load(f)

word2id = {k: (v + 3) for k, v in word_index.items()}
word2id['<PAD>'] = 0
word2id['<START>'] = 1
word2id['<UNK>'] = 2
word2id['<UNUSED>'] = 3

vocab_size = len(word2id)
print(f"词典大小: {vocab_size}")

# ===================== 3. 数据预处理 =====================
print("\n【第3步】数据预处理")
print("-" * 40)

train_text = [[idx + 3 for idx in text] for text in train_x]
test_text = [[idx + 3 for idx in text] for text in test_x]

maxlen = 256
train_data = tf.keras.preprocessing.sequence.pad_sequences(
    train_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)
test_data = tf.keras.preprocessing.sequence.pad_sequences(
    test_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)

print(f"训练数据形状: {train_data.shape}")
print(f"测试数据形状: {test_data.shape}")

# ===================== 4. 构建LSTM模型 =====================
print("\n【第4步】构建LSTM模型")
print("-" * 40)

def build_lstm_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, 128, input_length=maxlen),
        tf.keras.layers.LSTM(
            units=64,
            return_sequences=False,
            dropout=0.2,
            recurrent_dropout=0.2
        ),
        tf.keras.layers.Dropout(0.5),  # 添加额外的Dropout层
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

lstm_model = build_lstm_model()
lstm_model.summary()

# ===================== 5. 编译模型 =====================
print("\n【第5步】编译模型")
print("-" * 40)

# 使用学习率衰减
initial_learning_rate = 0.001
lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(
    initial_learning_rate, decay_steps=1000, decay_rate=0.9
)

lstm_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=lr_schedule),
    loss=tf.keras.losses.BinaryCrossentropy(),
    metrics=['accuracy']
)

# ===================== 6. 训练模型 =====================
print("\n【第6步】训练LSTM模型")
print("-" * 40)

EPOCHS = 5
BATCH_SIZE = 64

# 添加早停和模型检查点
callbacks = [
    tf.keras.callbacks.EarlyStopping(patience=2, restore_best_weights=True),
    tf.keras.callbacks.ModelCheckpoint('best_lstm_model.h5', save_best_only=True)
]

history_lstm = lstm_model.fit(
    train_data, train_y,
    batch_size=BATCH_SIZE,
    epochs=EPOCHS,
    validation_split=0.2,
    callbacks=callbacks,
    verbose=1
)

# ===================== 7. 评估模型 =====================
print("\n【第7步】评估LSTM模型")
print("-" * 40)

test_loss, test_acc = lstm_model.evaluate(test_data, test_y, verbose=0)
print(f"测试集损失: {test_loss:.4f}")
print(f"测试集准确率: {test_acc:.4f}")
print(f"LSTM准确率: {test_acc * 100:.2f}%")

# 保存模型和训练历史
lstm_model.save('lstm_model.h5')
np.save('lstm_history.npy', history_lstm.history)

print("\nLSTM模型训练完成!")
第三部分:GRU模型训练
  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# part3_gru.py
# IMDB情感分析 - GRU模型

import tensorflow as tf
import numpy as np
import json
import matplotlib.pyplot as plt

# 配置matplotlib中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False

print("=" * 60)
print("IMDB情感分析 - GRU模型")
print("=" * 60)

# ===================== 1. 加载数据 =====================
print("\n【第1步】加载IMDB数据集")
print("-" * 40)

X = np.load('../data/imdb.npz', allow_pickle=True)
train_x = X['x_train']
test_x = X['x_test']
train_y = X['y_train']
test_y = X['y_test']

print(f"训练集样本数: {len(train_x)}")
print(f"测试集样本数: {len(test_x)}")

# ===================== 2. 加载词典 =====================
print("\n【第2步】加载词典")
print("-" * 40)

path = '../data/imdb_word_index.json'
with open(path, 'r', encoding='utf-8') as f:
    word_index = json.load(f)

word2id = {k: (v + 3) for k, v in word_index.items()}
word2id['<PAD>'] = 0
word2id['<START>'] = 1
word2id['<UNK>'] = 2
word2id['<UNUSED>'] = 3

vocab_size = len(word2id)
print(f"词典大小: {vocab_size}")

# ===================== 3. 数据预处理 =====================
print("\n【第3步】数据预处理")
print("-" * 40)

train_text = [[idx + 3 for idx in text] for text in train_x]
test_text = [[idx + 3 for idx in text] for text in test_x]

maxlen = 256
train_data = tf.keras.preprocessing.sequence.pad_sequences(
    train_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)
test_data = tf.keras.preprocessing.sequence.pad_sequences(
    test_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)

print(f"训练数据形状: {train_data.shape}")
print(f"测试数据形状: {test_data.shape}")

# ===================== 4. 构建GRU模型 =====================
print("\n【第4步】构建GRU模型")
print("-" * 40)

def build_gru_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, 128, input_length=maxlen),
        tf.keras.layers.GRU(
            units=64,
            return_sequences=False,
            dropout=0.2,
            recurrent_dropout=0.2
        ),
        tf.keras.layers.BatchNormalization(),  # 添加批归一化
        tf.keras.layers.Dropout(0.5),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

gru_model = build_gru_model()
gru_model.summary()

# ===================== 5. 编译模型 =====================
print("\n【第5步】编译模型")
print("-" * 40)

# 使用AdamW优化器(带权重衰减)
gru_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
    loss=tf.keras.losses.BinaryCrossentropy(),
    metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()]
)

# ===================== 6. 训练模型 =====================
print("\n【第6步】训练GRU模型")
print("-" * 40)

EPOCHS = 5
BATCH_SIZE = 64

# 添加多个回调函数
callbacks = [
    tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True),
    tf.keras.callbacks.ModelCheckpoint('best_gru_model.h5', save_best_only=True),
    tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=2)
]

history_gru = gru_model.fit(
    train_data, train_y,
    batch_size=BATCH_SIZE,
    epochs=EPOCHS,
    validation_split=0.2,
    callbacks=callbacks,
    verbose=1
)

# ===================== 7. 评估模型 =====================
print("\n【第7步】评估GRU模型")
print("-" * 40)

test_loss, test_acc, test_precision, test_recall = gru_model.evaluate(test_data, test_y, verbose=0)
print(f"测试集损失: {test_loss:.4f}")
print(f"测试集准确率: {test_acc:.4f}")
print(f"测试集精确率: {test_precision:.4f}")
print(f"测试集召回率: {test_recall:.4f}")
print(f"GRU准确率: {test_acc * 100:.2f}%")

# 保存模型和训练历史
gru_model.save('gru_model.h5')
np.save('gru_history.npy', history_gru.history)

print("\nGRU模型训练完成!")
第四部分:模型对比和预测
  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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# part4_compare_and_predict.py
# 模型对比和情感预测

import tensorflow as tf
import numpy as np
import json
import matplotlib.pyplot as plt

# 配置matplotlib中文显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei']
plt.rcParams['axes.unicode_minus'] = False

print("=" * 60)
print("IMDB情感分析 - 模型对比与预测")
print("=" * 60)

# ===================== 加载保存的模型和历史 =====================
print("\n【第1步】加载训练好的模型")
print("-" * 40)

# 加载模型
simple_rnn_model = tf.keras.models.load_model('simple_rnn_model.h5')
lstm_model = tf.keras.models.load_model('lstm_model.h5')
gru_model = tf.keras.models.load_model('gru_model.h5')

# 加载训练历史
simple_history = np.load('simple_rnn_history.npy', allow_pickle=True).item()
lstm_history = np.load('lstm_history.npy', allow_pickle=True).item()
gru_history = np.load('gru_history.npy', allow_pickle=True).item()

# ===================== 重新加载数据用于预测 =====================
print("\n【第2步】加载数据")
print("-" * 40)

X = np.load('../data/imdb.npz', allow_pickle=True)
test_x = X['x_test']
test_y = X['y_test']

# 加载词典
path = '../data/imdb_word_index.json'
with open(path, 'r', encoding='utf-8') as f:
    word_index = json.load(f)

word2id = {k: (v + 3) for k, v in word_index.items()}
word2id['<PAD>'] = 0
word2id['<START>'] = 1
word2id['<UNK>'] = 2

id2word = {v: k for k, v in word2id.items()}
vocab_size = len(word2id)
maxlen = 256

# 预处理测试数据
test_text = [[idx + 3 for idx in text] for text in test_x]
test_data = tf.keras.preprocessing.sequence.pad_sequences(
    test_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=maxlen
)

# ===================== 模型评估对比 =====================
print("\n【第3步】模型性能对比")
print("-" * 40)

# 评估三个模型
simple_loss, simple_acc = simple_rnn_model.evaluate(test_data, test_y, verbose=0)
lstm_loss, lstm_acc = lstm_model.evaluate(test_data, test_y, verbose=0)
gru_loss, gru_acc = gru_model.evaluate(test_data, test_y, verbose=0)

print("\n=== 模型性能对比 ===")
print(f"SimpleRNN - 损失: {simple_loss:.4f}, 准确率: {simple_acc*100:.2f}%")
print(f"LSTM     - 损失: {lstm_loss:.4f}, 准确率: {lstm_acc*100:.2f}%")
print(f"GRU      - 损失: {gru_loss:.4f}, 准确率: {gru_acc*100:.2f}%")

# ===================== 可视化对比 =====================
print("\n【第4步】绘制对比图表")
print("-" * 40)

plt.figure(figsize=(15, 10))

# 损失曲线
plt.subplot(2, 2, 1)
plt.plot(simple_history['val_loss'], label='SimpleRNN', marker='o')
plt.plot(lstm_history['val_loss'], label='LSTM', marker='s')
plt.plot(gru_history['val_loss'], label='GRU', marker='^')
plt.xlabel('训练轮数')
plt.ylabel('验证损失')
plt.title('验证损失对比')
plt.legend()
plt.grid(True)

# 准确率曲线
plt.subplot(2, 2, 2)
plt.plot(simple_history['val_accuracy'], label='SimpleRNN', marker='o')
plt.plot(lstm_history['val_accuracy'], label='LSTM', marker='s')
plt.plot(gru_history['val_accuracy'], label='GRU', marker='^')
plt.xlabel('训练轮数')
plt.ylabel('验证准确率')
plt.title('验证准确率对比')
plt.legend()
plt.grid(True)

# 最终准确率柱状图
plt.subplot(2, 2, 3)
models = ['SimpleRNN', 'LSTM', 'GRU']
accuracies = [simple_acc, lstm_acc, gru_acc]
colors = ['skyblue', 'lightcoral', 'lightgreen']
bars = plt.bar(models, accuracies, color=colors)
plt.ylim(0, 1)
plt.ylabel('准确率')
plt.title('测试集准确率对比')
for bar, acc in zip(bars, accuracies):
    plt.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01,
             f'{acc * 100:.1f}%', ha='center', va='bottom')

# 训练时间对比(如果有记录)
plt.subplot(2, 2, 4)
# 这里假设有训练时间记录,如果没有可以省略
plt.text(0.5, 0.5, 'GRU训练速度最快\nLSTM效果最好\nSimpleRNN基础模型', 
         ha='center', va='center', fontsize=12,
         bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
plt.axis('off')
plt.title('模型特点总结')

plt.tight_layout()
plt.savefig('model_comparison.png', dpi=100, bbox_inches='tight')
plt.show()

# ===================== 情感预测函数 =====================
print("\n【第5步】情感预测功能")
print("-" * 40)

def text_to_sequence(text, word2id, maxlen=256):
    """将文本转换为模型可接受的序列"""
    words = text.lower().split()
    sequence = []
    for word in words:
        # 简单的单词匹配,实际应用中需要更复杂的文本预处理
        if word in word2id:
            sequence.append(word2id[word])
        else:
            sequence.append(word2id['<UNK>'])
    
    # 填充或截断序列
    if len(sequence) > maxlen:
        sequence = sequence[:maxlen]
    else:
        sequence = sequence + [word2id['<PAD>']] * (maxlen - len(sequence))
    
    return np.array([sequence])

def predict_sentiment(review_text, model, model_name="GRU"):
    """预测评论情感"""
    # 重新加载词典(避免作用域问题)
    with open('../data/imdb_word_index.json', 'r', encoding='utf-8') as f:
        word_index = json.load(f)
    
    word2id_local = {k: (v + 3) for k, v in word_index.items()}
    word2id_local['<PAD>'] = 0
    word2id_local['<START>'] = 1
    word2id_local['<UNK>'] = 2
    
    # 转换文本
    sequence = text_to_sequence(review_text, word2id_local, maxlen=256)
    
    # 预测
    prediction = model.predict(sequence, verbose=0)
    sentiment = "正面情感 😊" if prediction[0][0] > 0.5 else "负面情感 😞"
    confidence = prediction[0][0] if prediction[0][0] > 0.5 else 1 - prediction[0][0]
    
    return sentiment, confidence, prediction[0][0]

# 选择最佳模型(这里使用LSTM,因为通常效果最好)
best_model = lstm_model

# 测试示例
print("\n=== 预测示例 ===")

# 正面评论示例
positive_reviews = [
    "This movie is absolutely fantastic! I loved every minute of it. The acting was superb and the story was engaging.",
    "Excellent film! Great performances and beautiful cinematography. Highly recommended!",
    "I really enjoyed this movie. It was entertaining from start to finish."
]

# 负面评论示例
negative_reviews = [
    "Terrible movie, complete waste of time. I regret watching it. The plot made no sense.",
    "Awful acting and boring story. I fell asleep halfway through. Don't waste your money.",
    "This is the worst movie I have ever seen. Nothing good about it at all."
]

print("\n--- 正面评论测试 ---")
for i, review in enumerate(positive_reviews, 1):
    sentiment, confidence, score = predict_sentiment(review, best_model)
    print(f"\n评论{i}: {review[:80]}...")
    print(f"预测结果: {sentiment}")
    print(f"置信度: {confidence*100:.1f}%")
    print(f"情感分数: {score:.4f}")

print("\n--- 负面评论测试 ---")
for i, review in enumerate(negative_reviews, 1):
    sentiment, confidence, score = predict_sentiment(review, best_model)
    print(f"\n评论{i}: {review[:80]}...")
    print(f"预测结果: {sentiment}")
    print(f"置信度: {confidence*100:.1f}%")
    print(f"情感分数: {score:.4f}")

# ===================== 交互式预测 =====================
print("\n【第6步】交互式预测")
print("-" * 40)

def interactive_prediction():
    """交互式情感预测"""
    print("\n=== 交互式情感分析 ===")
    print("输入电影评论,系统将预测情感倾向(输入 'quit' 退出)")
    
    while True:
        print("\n" + "-" * 40)
        review = input("请输入电影评论: ")
        
        if review.lower() == 'quit':
            print("感谢使用!再见!")
            break
        
        if len(review.strip()) == 0:
            print("请输入有效的评论内容!")
            continue
        
        sentiment, confidence, score = predict_sentiment(review, best_model)
        print(f"\n评论: {review}")
        print(f"情感分析结果: {sentiment}")
        print(f"置信度: {confidence*100:.1f}%")
        print(f"情感分数: {score:.4f} (0-负面, 1-正面)")

# 取消下面的注释以启用交互式预测
# interactive_prediction()

print("\n" + "=" * 60)
print("IMDB文本情感分析项目完成!")
print("模型对比图已保存为 'model_comparison.png'")
print("=" * 60)

3.5.4 代码核心功能讲解

第1-3步:数据加载与预处理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
## 加载IMDB数据集
X = np.load('../data/imdb.npz', allow_pickle=True)
train_x = X['x_train']  ## 训练集评论(数字索引序列)
test_x = X['x_test']    ## 测试集评论
train_y = X['y_train']  ## 训练集标签(0或1)
test_y = X['y_test']    ## 测试集标签

## 构建词典映射
word2id = {k: (v + 3) for k, v in word_index.items()}
word2id['<PAD>'] = 0   ## 填充标记
word2id['<START>'] = 1 ## 开始标记
word2id['<UNK>'] = 2   ## 未知词标记

## 序列填充,统一长度
train_data = tf.keras.preprocessing.sequence.pad_sequences(
    train_text, value=word2id['<PAD>'],
    padding='post', truncating='post', maxlen=256
)

代码功能说明

  • IMDB数据已预处理为数字索引序列,每个数字代表一个单词
  • 索引需要+3,为特殊标记(PAD、START、UNK、UNUSED)留出空间
  • pad_sequences()将不同长度的序列统一为固定长度256
    • padding='post':在序列末尾填充
    • truncating='post':从末尾截断超长序列
    • value=0:使用0作为填充值
第4步:构建SimpleRNN模型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def build_simple_rnn_model():
    model = tf.keras.Sequential([
        ## Embedding层:将单词索引转换为128维词向量
        tf.keras.layers.Embedding(
            input_dim=vocab_size,  ## 词典大小
            output_dim=128,         ## 词向量维度
            input_length=maxlen     ## 序列长度
        ),
        ## SimpleRNN层:处理序列
        tf.keras.layers.SimpleRNN(
            units=64,               ## 隐藏状态维度
            return_sequences=False, ## 只返回最后一个输出
            dropout=0.2            ## Dropout防止过拟合
        ),
        ## 输出层:sigmoid输出0-1之间的概率
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

网络结构说明

输入形状 输出形状 参数量 功能
Embedding (batch, 256) (batch, 256, 128) vocab_size×128 单词→向量
SimpleRNN (batch, 256, 128) (batch, 64) 128×64+64×64+64=12,352 序列特征提取
Dense (batch, 64) (batch, 1) 64+1=65 二分类输出
第5步:编译模型
1
2
3
4
5
simple_rnn_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
    loss=tf.keras.losses.BinaryCrossentropy(),
    metrics=['accuracy']
)

参数说明

  • optimizer=‘adam’:Adam优化器,自适应学习率,收敛快
  • loss=‘binary_crossentropy’:二元交叉熵损失,适用于二分类问题
  • metrics=[‘accuracy’]:监控准确率指标
第6步:训练模型
1
2
3
4
5
6
7
history_simple = simple_rnn_model.fit(
    train_data, train_y,
    batch_size=64,        ## 每批处理64条评论
    epochs=5,             ## 训练5轮
    validation_split=0.2, ## 20%数据作为验证集
    verbose=1             ## 显示进度条
)

训练过程说明

  • 每轮训练会计算损失和准确率
  • 验证集用于监控模型是否过拟合
  • 训练历史保存在history对象中,可用于可视化
第8-9步:LSTM和GRU模型
 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
## LSTM模型 - 解决长距离依赖问题
def build_lstm_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, 128, input_length=maxlen),
        tf.keras.layers.LSTM(
            units=64,
            return_sequences=False,
            dropout=0.2,
            recurrent_dropout=0.2  ## 循环连接的dropout
        ),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

## GRU模型 - LSTM的简化版本
def build_gru_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, 128, input_length=maxlen),
        tf.keras.layers.GRU(
            units=64,
            return_sequences=False,
            dropout=0.2,
            recurrent_dropout=0.2
        ),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

三种RNN对比

模型 门控数量 参数量 训练速度 长序列性能
SimpleRNN 0 最少 最快
LSTM 3 最多 最慢 最好
GRU 2 中等 中等 较好
模型结构总结

本项目采用的RNN模型结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
## 完整的情感分析模型结构
model = tf.keras.Sequential([
    ## 1. 输入层 + Embedding层:将文本转换为向量
    tf.keras.layers.Embedding(
        input_dim=vocab_size,   ## 词典大小:约88,000
        output_dim=128,          ## 词向量维度
        input_length=256         ## 序列长度
    ),
    
    ## 2. RNN层:提取序列特征(可选SimpleRNN/LSTM/GRU)
    tf.keras.layers.LSTM(
        units=64,                ## 隐藏状态维度
        return_sequences=False,  ## 只取最后输出
        dropout=0.2,            ## 输入dropout
        recurrent_dropout=0.2   ## 循环dropout
    ),
    
    ## 3. 输出层:二分类
    tf.keras.layers.Dense(1, activation='sigmoid')
])

各层功能说明:

功能 说明
Embedding层 文本向量化 将单词索引转换为密集向量,语义相似的单词向量相近
LSTM/GRU层 序列建模 捕捉单词间的顺序依赖,理解上下文语义
Dropout 正则化 随机丢弃神经元,防止过拟合
输出层 分类决策 sigmoid将输出压缩到[0,1],表示正面情感概率

3.6 RNN项目练习

【练习1:调整序列长度】

修改maxlen参数,观察对模型性能的影响:

1
2
3
4
5
6
## 尝试不同的序列长度
max_lengths = [128, 256, 512]

for maxlen in max_lengths:
    train_data = pad_sequences(train_text, maxlen=maxlen, padding='post')
    ## 训练并记录准确率

思考:更长的序列能保留更多信息,但也会增加计算量。如何平衡?

【练习2:调整词向量维度】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## 尝试不同的词向量维度
embedding_dims = [64, 128, 256]

for emb_dim in embedding_dims:
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, emb_dim, input_length=256),
        tf.keras.layers.LSTM(64),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    ## 训练并记录准确率

【练习3:双向RNN】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## 使用双向LSTM,同时从前向后和从后向前处理序列
def build_bidirectional_lstm():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, 128, input_length=256),
        tf.keras.layers.Bidirectional(
            tf.keras.layers.LSTM(64, return_sequences=False)
        ),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

代码功能说明

  • 双向RNN:同时从前向后和从后向前读取序列,能捕捉更完整的上下文信息
  • 例如:“这部电影不怎么样"中的"不"字,从后向前读更容易识别为否定词

【练习4:堆叠RNN层】

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
## 使用多层LSTM
def build_stacked_lstm():
    model = tf.keras.Sequential([
        tf.keras.layers.Embedding(vocab_size, 128, input_length=256),
        ## 第一层LSTM返回全部输出
        tf.keras.layers.LSTM(64, return_sequences=True, dropout=0.2),
        ## 第二层LSTM只返回最后输出
        tf.keras.layers.LSTM(32, return_sequences=False, dropout=0.2),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    return model

代码功能说明

  • 堆叠LSTM:多层LSTM叠加,每层学习不同抽象层次的特征
  • 第一层return_sequences=True将全部输出传递给第二层
  • 堆叠层数越多,模型表达能力越强,但训练也更困难

本章小结

网络类型 核心特点 主要应用 优缺点
CNN 局部连接、权值共享 图像分类、目标检测 擅长提取空间特征
RNN 循环结构、记忆机制 文本分析、时间序列 擅长处理序列数据
LSTM 三门控结构 长文本、语音识别 解决长距离依赖
GRU 两门控结构 中等长度序列 训练更快,效果接近LSTM
0%