趋近智
在构建像多层感知器 (MLP)、卷积神经网络 (neural network) (CNN) 或循环神经网络 (RNN) 这样复杂的神经网络架构之前,您必须先确保您的数据处于合适的状态。“垃圾进,垃圾出”这句老话在深度学习 (deep learning)中尤其适用。高质量、准备得当的数据是训练高效模型的前提。将介绍使用 Julia 强大的数据操作和科学计算环境进行数据准备和预处理的基本方法。这将包括清理数据、转换特征以及构建数据集,使其可供 Flux.jl 使用。
数据准备是指将原始数据集转换为神经网络 (neural network)可以处理的干净、有结构的数据格式。这通常包括以下几个步骤:
DataFrame。Julia 凭借 DataFrames.jl、CSV.jl、Statistics.jl 和 MLUtils.jl 等包,为这些任务提供了高效且富有表达力的环境。其性能特点在处理深度学习 (deep learning)中常见的海量数据集时尤其有利。
我们来看一个典型的数据准备流程。
深度学习项目中数据准备的一般流程。
神经网络 (neural network)对输入数据的呈现方式很敏感。以下是一些您会遇到的常见预处理操作。
"数据集常含有缺失值。您如何处理它们会明显影响模型性能。"
识别:在 DataFrames.jl 中,缺失值由 missing 表示。您可以使用 ismissing() 等函数来识别它们,或者使用 describe(df, :nmissing) 获取摘要。
using DataFrames, Statistics
# 包含缺失值的示例 DataFrame
data = DataFrame(A = [1, 2, missing, 4, 5], B = [missing, 0.2, 0.3, 0.4, 0.5])
println(describe(data, :nmissing))
策略:
dropmissing(df))。如果某列有太多缺失值且不那么重要,您可以删除该列。# 用列 A 的均值填充缺失值
mean_A = mean(skipmissing(data.A))
data.A = coalesce.(data.A, mean_A)
# 用特定值(例如 0.0)填充列 B 的缺失值
replace!(data.B, missing => 0.0)
println(data)
coalesce 函数很实用,因为它返回第一个非 missing 参数 (parameter)。
神经网络,特别是那些使用基于梯度的优化器训练的神经网络,当输入特征在相似的尺度上时,通常表现更好,收敛更快。特征范围的较大差异可能导致训练过程不稳定。
最小-最大缩放(归一化 (normalization)):将特征重新缩放到固定范围,通常是 [0, 1] 或 [-1, 1]。缩放到 [0, 1] 的公式是:
其中 和 分别是该特征的最小值和最大值。
# 数值特征示例
feature = [10.0, 20.0, 15.0, 30.0, 25.0]
function min_max_scale(X)
X_min = minimum(X)
X_max = maximum(X)
return (X .- X_min) ./ (X_max - X_min)
end
scaled_feature = min_max_scale(feature)
# scaled_feature 将是 [0.0, 0.5, 0.25, 1.0, 0.75]
println("原始数据: ", feature)
println("最小-最大缩放后: ", scaled_feature)
标准化(Z-分数归一化):将特征重新缩放,使其均值 () 为 0,标准差 () 为 1。公式是:
与最小-最大缩放相比,此方法受异常值的影响较小。
using Statistics
# 数值特征示例
feature = [10.0, 20.0, 15.0, 30.0, 25.0]
function standardize(X)
mu = mean(X)
sigma = std(X)
return (X .- mu) ./ sigma
end
standardized_feature = standardize(feature)
println("标准化后: ", standardized_feature)
# 检查均值和标准差(应接近 0 和 1)
println("标准化后的均值: ", mean(standardized_feature))
println("标准化后的标准差: ", std(standardized_feature))
最小-最大缩放和标准化之间的选择取决于数据和神经网络架构。对于图像,像素值通常缩放到 [0, 1]。对于其他类型的数据,标准化是常见的默认设置。
特征值在最小-最大缩放前后的比较。缩放后的值被映射到辅助 y 轴上的 [0,1] 范围。
神经网络需要数值输入。类别特征(例如“颜色”:“红色”、“蓝色”、“绿色”,或“城市”:“纽约”、“伦敦”)必须转换为数值格式。
整数编码(标签编码):为每个类别分配一个唯一的整数。例如,“红色” -> 0,“蓝色” -> 1,“绿色” -> 2。 这种方法简单,但可能暗示一个不存在的序数关系(例如,绿色 > 蓝色 > 红色)。它适用于序数数据(例如,“低”、“中”、“高”)。
categories = ["cat", "dog", "bird", "cat", "dog"]
unique_cats = unique(categories)
cat_to_int = Dict(c => i for (i, c) in enumerate(unique_cats))
encoded_integers = [cat_to_int[c] for c in categories]
# 示例: 如果 unique_cats = ["cat", "dog", "bird"]
# cat_to_int = Dict("cat"=>1, "dog"=>2, "bird"=>3)
# encoded_integers = [1, 2, 3, 1, 2]
println("整数编码后: ", encoded_integers)
独热编码:为每个唯一的类别创建一个新的二进制(0 或 1)特征。对于给定样本,对应其类别的特征为 1,所有其他特征为 0。
示例:
“红色” -> [1, 0, 0]
“蓝色” -> [0, 1, 0]
“绿色” -> [0, 0, 1]
这种方法避免了强加人为的顺序,但如果唯一类别很多,可能会导致高维特征空间。Flux.jl 提供了 Flux.onehot 和 Flux.onehotbatch 来实现此功能。
using Flux
# 使用与上述相同的类别
# 假设 unique_cats = ["cat", "dog", "bird"]
# Flux.onehotbatch(data, labels)
# 'data' 是类别向量,'labels' 是唯一的、已排序的类别列表
one_hot_encoded = Flux.onehotbatch(categories, unique_cats)
# 这会生成一个 Flux.OneHotMatrix
# 若要将其视为常规矩阵:
# Matrix(one_hot_encoded)
# 对于 categories = ["cat", "dog", "bird", "cat", "dog"]
# 以及 unique_cats = ["bird", "cat", "dog"] 的结果 (如果未提供排序,Flux 会内部排序标签)
# 它将是:
# 0 1 0 0 1
# 1 0 0 1 0
# 0 0 1 0 0
# (如果 "bird" 是标签 1, "cat" 标签 2, "dog" 标签 3 (排序后))
# 为清晰起见,我们明确定义标签
explicit_labels = ["cat", "dog", "bird"] # 顺序对于解释很重要
one_hot_encoded_explicit = Flux.onehotbatch(categories, explicit_labels)
println("独热编码后 (矩阵表示):")
display(Matrix(one_hot_encoded_explicit)) # display 可以很好地打印矩阵
# explicit_labels = ["cat", "dog", "bird"] 时的输出:
# 1 0 0 1 0 (cat)
# 0 1 0 0 1 (dog)
# 0 0 1 0 0 (bird)
对于基数非常高(许多唯一值)的特征,例如用户 ID 或词汇表 (vocabulary)中的单词,整数编码和独热编码都不是理想选择。在这种情况下,通常使用嵌入 (embedding)层,我们将在本章后面讨论。
特征清理和转换后,整个数据集需要构建成 Flux.jl 模型可以处理的格式。这通常意味着将数据转换为特定数值类型的 Julia Array,通常是 Float32,以提高深度学习 (deep learning)库和 GPU 的效率。
输入形状:不同的神经网络层期望特定维度的输入:
(width, height, channels, batch_size),通常缩写为 WHCN。重塑数据:您通常会使用 reshape 来将数据调整为正确的维度。例如,如果您有一组扁平的图像向量,您可以将它们重塑为 CNN 的 WHCN 格式。
# 假设您有 100 张图像,每张都是 28x28 灰度图
# 并且它们被加载为 100 个矩阵(28x28)的向量
# images_vector = [rand(Float32, 28, 28) for _ in 1:100];
# 对于 Flux,最好使用单个 4D 数组:(W, H, C, N)
# W=28, H=28, C=1(灰度图), N=100
# 示例:
num_samples = 100
img_width = 28
img_height = 28
channels = 1 # 灰度图
# 扁平化数据(例如,来自 CSV,其中每行都是一张图像)
# 每行有 28*28 = 784 像素。共 100 行。
# 如果特征是行,这将是 784x100 矩阵
# 如果样本是行,则是 100x784。假设样本是行。
flat_data_as_rows = rand(Float32, num_samples, img_width * img_height)
# 要与 Flux Dense 层一起使用,我们需要特征 x 样本:(784, 100)
data_for_dense = permutedims(flat_data_as_rows, (2,1)) # 变为 784x100
# 要与 Flux Conv 层一起使用,我们需要 WHCN:(28, 28, 1, 100)
# 首先,确保数据是 (features, samples) 即 784x100
# 然后将每列(样本)重塑为 WxHxC
data_for_cnn = reshape(data_for_dense, img_width, img_height, channels, num_samples)
println("Dense 层的形状: ", size(data_for_dense))
println("CNN 层的形状: ", size(data_for_cnn))
数据类型转换:确保您的数据类型为 Float32(如果精度非常重要,则使用 Float64,但 Float32 是深度学习的标准)。
# 如果 data_matrix 是 Array{Float64, 2}
# data_matrix_f32 = Float32.(data_matrix)
训练前的一个重要步骤是将数据集分成至少两个,最好是三个子集:
常见的分割比例是训练集占 60-80%,验证集占 10-20%,测试集占 10-20%。MLUtils.jl 提供了 splitobs 来实现此功能。
using MLUtils
# 假设 `features` 是您的输入数据(例如,一个矩阵)
# `labels` 是您的目标数据(例如,一个向量或矩阵)
# features = rand(Float32, 10, 1000) # 10 个特征,1000 个样本
# labels = rand(Float32, 1, 1000) # 1 个输出,1000 个样本
# 示例:
num_total_samples = 1000
X_data = rand(Float32, 5, num_total_samples) # 5 个特征
Y_data = Flux.onehotbatch(rand(1:3, num_total_samples), 1:3) # 3 个类别
# 分割为训练集 (70%) 和测试集 (30%)
(X_train, Y_train), (X_test, Y_test) = splitobs((X_data, Y_data), at=0.7, shuffle=true)
# 进一步将测试集分割为验证集和测试集(例如,从原始数据中分出 15% 用于验证,15% 用于测试)
# 原始测试集占总数的 30%。我们想将其 50/50 分割。
# 因此,当前 X_test 的 0.5 份将用于验证。
(X_val, Y_val), (X_test_final, Y_test_final) = splitobs((X_test, Y_test), at=0.5, shuffle=false) # 通常这里不需要打乱
println("训练样本数: ", size(X_train, 2))
println("验证样本数: ", size(X_val, 2))
println("测试样本数: ", size(X_test_final, 2))
在分割时,特别是对于类别不平衡的分类任务,使用分层抽样是一种好的做法。这有助于保证每个类别的比例在训练集、验证集和测试集中大致相同。如果标签提供得当,MLUtils.splitobs 通常可以处理这种情况,或者您可能需要使用 MLJ.jl 中更专业的工具,或实现自定义逻辑进行分层。
让我们用一个小型数据集将其中一些想法结合起来。假设我们有关于水果的数据:
| 重量 (克) | 颜色 | 质地 | 是否甜 |
|---|---|---|---|
| 150 | 红色 | 光滑 | 1 |
| 120 | 绿色 | 光滑 | 0 |
| 160 | 红色 | 凹凸 | 1 |
| missing | 黄色 | 光滑 | 1 |
我们的目标是预测 IsSweet(1 表示甜,0 表示不甜)。
加载和表示数据(为清晰起见,使用 DataFrame)
using DataFrames, Statistics, Flux
df = DataFrame(
Weight = [150.0, 120.0, 160.0, missing],
Color = ["Red", "Green", "Red", "Yellow"],
Texture = ["Smooth", "Smooth", "Bumpy", "Smooth"],
IsSweet = [1, 0, 1, 1]
)
处理缺失值(填充 Weight)
mean_weight = mean(skipmissing(df.Weight))
df.Weight = coalesce.(df.Weight, mean_weight)
println("处理缺失值后的 DataFrame:")
display(df)
特征缩放(最小-最大缩放 Weight)
# 使用之前定义的函数
function min_max_scale_col(X_col)
X_min = minimum(X_col)
X_max = maximum(X_col)
return (X_col .- X_min) ./ (X_max - X_min)
end
df.Weight_scaled = min_max_scale_col(df.Weight)
编码类别特征(Color、Texture)使用独热编码
unique_colors = unique(df.Color)
color_onehot = Flux.onehotbatch(df.Color, unique_colors)
unique_textures = unique(df.Texture)
texture_onehot = Flux.onehotbatch(df.Texture, unique_textures)
# 将 OneHotArrays 转换为常规矩阵以便组合
color_matrix = Float32.(Matrix(color_onehot))
texture_matrix = Float32.(Matrix(texture_onehot))
将特征组合成一个矩阵(特征 x 样本)
# 缩放后的重量(1 个特征)
weight_feature_matrix = reshape(Float32.(df.Weight_scaled), 1, nrow(df))
# 组合所有特征矩阵
# 特征是行,样本是列
X_matrix = vcat(weight_feature_matrix, color_matrix, texture_matrix)
println("\n最终特征矩阵 X (Float32):")
display(X_matrix)
println("X 的大小: ", size(X_matrix))
# 准备标签 Y(1 x 样本)
Y_matrix = reshape(Float32.(df.IsSweet), 1, nrow(df))
println("\n标签矩阵 Y (Float32):")
display(Y_matrix)
println("Y 的大小: ", size(Y_matrix))
这个 X_matrix(特征)和 Y_matrix(标签)现在处于适合作为 Flux 模型输入的格式。它们是 Float32 数组,X_matrix 中的特征按行排列,样本按列排列。
严谨的数据准备和预处理是根本。这些步骤可以保证您的模型以信息丰富且计算友好的格式接收数据。在数据清理、转换并正确构建之后,您现在可以考虑如何在训练期间高效地加载和处理它,这是下一节关于 MLUtils.jl 的主题。
这部分内容有帮助吗?
© 2026 ApX Machine LearningAI伦理与透明度•