1. KNN算法与多变量时间序列预测基础
K最近邻(K-Nearest Neighbors, KNN)是一种经典的惰性学习算法,在时间序列预测领域展现出独特的优势。与需要复杂训练的模型不同,KNN通过搜索历史数据中最相似的片段来进行预测,这种特性使其特别适合处理具有周期性和重复模式的时间序列数据。
多变量时间序列预测相比单变量情况更为复杂,我们需要同时考虑多个相关变量的历史信息。假设我们有一个包含温度、湿度和风速的气象数据集,KNN算法可以找到与当前气象条件相似的历史时刻,然后基于这些"邻居"的未来值来预测接下来可能的气象变化。
在Matlab中实现KNN时间序列预测时,不使用工具箱意味着我们需要手动完成以下核心步骤:
- 数据预处理:包括归一化、处理缺失值等
- 特征工程:构建适合KNN算法的特征表示
- 相似度计算:定义和实现距离度量方法
- 邻居搜索:高效查找最近的K个邻居
- 预测生成:基于邻居的未来值进行预测
提示:KNN算法的时间复杂度与数据量直接相关,对于大规模时间序列数据,需要考虑优化搜索效率的方法。
2. 数据准备与预处理
2.1 数据加载与结构设计
首先我们需要准备多变量时间序列数据。假设我们有一个包含三个变量的数据集(例如温度、湿度、压力),存储在一个N×3的矩阵中,其中N是时间步数:
matlab复制% 示例数据加载 - 假设数据存储在CSV文件中
data = csvread('multivariate_data.csv');
time_steps = size(data, 1);
num_variables = size(data, 2);
对于时间序列预测,我们需要构建一个滑动窗口的数据结构。给定窗口大小W,我们需要将原始序列转换为一系列窗口样本:
matlab复制function [X, Y] = create_sliding_windows(data, window_size, horizon)
num_samples = size(data, 1) - window_size - horizon + 1;
X = zeros(num_samples, window_size * size(data, 2));
Y = zeros(num_samples, size(data, 2));
for i = 1:num_samples
window = data(i:i+window_size-1, :);
target = data(i+window_size:i+window_size+horizon-1, :);
X(i, :) = window(:)'; % 将窗口展平为一行
Y(i, :) = mean(target, 1); % 取预测时段的平均值
end
end
2.2 数据标准化处理
多变量时间序列通常包含不同量纲的变量,标准化是必不可少的步骤:
matlab复制function [normalized_data, mu, sigma] = normalize_data(data)
mu = mean(data, 1);
sigma = std(data, 0, 1);
normalized_data = (data - mu) ./ sigma;
end
% 使用示例
[normalized_data, mu, sigma] = normalize_data(data);
window_size = 10; % 滑动窗口大小
horizon = 3; % 预测步长
[X, Y] = create_sliding_windows(normalized_data, window_size, horizon);
注意:标准化参数(均值和标准差)需要保存,以便对新数据进行相同的转换,并在预测后将结果反标准化。
3. KNN核心算法实现
3.1 距离度量与相似度计算
KNN算法的核心是距离度量。对于多变量时间序列,我们通常使用动态时间规整(DTW)或欧氏距离:
matlab复制function distances = calculate_euclidean_distances(X_train, X_test)
% 计算测试样本与所有训练样本的欧氏距离
num_train = size(X_train, 1);
num_test = size(X_test, 1);
distances = zeros(num_test, num_train);
for i = 1:num_test
diff = X_train - repmat(X_test(i, :), num_train, 1);
distances(i, :) = sqrt(sum(diff.^2, 2))';
end
end
对于更复杂的时间序列模式,可以实现DTW距离:
matlab复制function dtw_dist = dtw_distance(seq1, seq2)
% 序列需要先reshape为原始窗口形状
seq1 = reshape(seq1, [], num_variables);
seq2 = reshape(seq2, [], num_variables);
n = size(seq1, 1);
m = size(seq2, 1);
dtw_matrix = inf(n+1, m+1);
dtw_matrix(1, 1) = 0;
for i = 2:n+1
for j = 2:m+1
cost = norm(seq1(i-1, :) - seq2(j-1, :));
dtw_matrix(i, j) = cost + min([dtw_matrix(i-1, j), ...
dtw_matrix(i, j-1), ...
dtw_matrix(i-1, j-1)]);
end
end
dtw_dist = dtw_matrix(n+1, m+1);
end
3.2 KNN搜索与预测生成
实现KNN搜索和预测的核心函数:
matlab复制function [neighbors_indices, neighbors_distances] = find_knn(X_train, X_test, k)
distances = calculate_euclidean_distances(X_train, X_test);
[sorted_dist, sorted_ind] = sort(distances, 2);
neighbors_indices = sorted_ind(:, 1:k);
neighbors_distances = sorted_dist(:, 1:k);
end
function predictions = knn_predict(Y_train, neighbors_indices, neighbors_distances)
[num_test, k] = size(neighbors_indices);
num_variables = size(Y_train, 2);
predictions = zeros(num_test, num_variables);
for i = 1:num_test
weights = 1./(neighbors_distances(i, :) + eps); % 避免除以零
weights = weights ./ sum(weights); % 归一化权重
neighbors = Y_train(neighbors_indices(i, :), :);
predictions(i, :) = sum(neighbors .* weights', 1);
end
end
4. 完整预测流程实现
4.1 训练-测试集划分
时间序列数据需要特别注意划分方式,不能随机打乱:
matlab复制function [X_train, Y_train, X_test, Y_test] = split_time_series(X, Y, train_ratio)
num_samples = size(X, 1);
train_size = floor(train_ratio * num_samples);
X_train = X(1:train_size, :);
Y_train = Y(1:train_size, :);
X_test = X(train_size+1:end, :);
Y_test = Y(train_size+1:end, :);
end
% 使用示例
train_ratio = 0.8;
[X_train, Y_train, X_test, Y_test] = split_time_series(X, Y, train_ratio);
4.2 预测流程封装
将整个预测流程封装为一个完整函数:
matlab复制function [predictions, actual] = knn_time_series_predict(data, window_size, horizon, k, train_ratio)
% 数据标准化
[normalized_data, mu, sigma] = normalize_data(data);
% 创建滑动窗口
[X, Y] = create_sliding_windows(normalized_data, window_size, horizon);
% 划分训练测试集
[X_train, Y_train, X_test, Y_test] = split_time_series(X, Y, train_ratio);
% KNN预测
[neighbors_indices, neighbors_distances] = find_knn(X_train, X_test, k);
normalized_predictions = knn_predict(Y_train, neighbors_indices, neighbors_distances);
% 反标准化
predictions = normalized_predictions .* sigma + mu;
actual = Y_test .* sigma + mu;
end
4.3 参数优化与交叉验证
为了找到最佳的K值和窗口大小,我们可以实现一个简单的交叉验证:
matlab复制function best_k = optimize_k(X_train, Y_train, max_k, num_folds)
num_samples = size(X_train, 1);
fold_size = floor(num_samples / num_folds);
k_values = 1:max_k;
errors = zeros(length(k_values), 1);
for k_idx = 1:length(k_values)
k = k_values(k_idx);
fold_errors = zeros(num_folds, 1);
for fold = 1:num_folds
val_start = (fold-1)*fold_size + 1;
val_end = min(fold*fold_size, num_samples);
val_indices = val_start:val_end;
train_indices = setdiff(1:num_samples, val_indices);
X_val = X_train(val_indices, :);
Y_val = Y_train(val_indices, :);
X_tr = X_train(train_indices, :);
Y_tr = Y_train(train_indices, :);
[neighbors_indices, neighbors_distances] = find_knn(X_tr, X_val, k);
pred = knn_predict(Y_tr, neighbors_indices, neighbors_distances);
fold_errors(fold) = mean(sqrt(mean((pred - Y_val).^2, 2)));
end
errors(k_idx) = mean(fold_errors);
end
[~, best_idx] = min(errors);
best_k = k_values(best_idx);
end
5. 性能评估与可视化
5.1 评估指标实现
实现常见的时间序列预测评估指标:
matlab复制function [mae, rmse, mape] = evaluate_predictions(actual, predictions)
errors = actual - predictions;
mae = mean(abs(errors));
rmse = sqrt(mean(errors.^2));
mape = mean(abs(errors ./ actual)) * 100;
end
5.2 结果可视化
创建预测结果的可视化展示:
matlab复制function plot_predictions(time, actual, predictions, variable_names)
num_variables = size(actual, 2);
figure('Position', [100, 100, 1200, 800]);
for i = 1:num_variables
subplot(num_variables, 1, i);
plot(time, actual(:, i), 'b-', 'LineWidth', 1.5);
hold on;
plot(time, predictions(:, i), 'r--', 'LineWidth', 1.5);
title(sprintf('%s预测结果', variable_names{i}));
xlabel('时间');
ylabel(variable_names{i});
legend({'实际值', '预测值'}, 'Location', 'best');
grid on;
end
end
% 使用示例
variable_names = {'温度', '湿度', '压力'};
time = (1:size(Y_test, 1))';
plot_predictions(time, actual, predictions, variable_names);
5.3 多步预测实现
扩展我们的方法以实现多步预测:
matlab复制function multi_step_predictions = knn_multi_step_predict(data, window_size, horizon, k, steps)
[normalized_data, mu, sigma] = normalize_data(data);
num_variables = size(data, 2);
% 初始化预测矩阵
multi_step_predictions = zeros(steps, num_variables);
current_window = normalized_data(end-window_size+1:end, :);
for step = 1:steps
% 展平当前窗口
current_window_flat = current_window(:)';
% 创建所有历史窗口
[X, ~] = create_sliding_windows(normalized_data, window_size, horizon);
% 找到最近邻
[neighbors_indices, neighbors_distances] = find_knn(X, current_window_flat, k);
% 获取邻居的下一个时间步
next_steps = normalized_data(neighbors_indices + window_size, :);
% 加权平均预测
weights = 1./(neighbors_distances + eps);
weights = weights ./ sum(weights);
prediction = sum(next_steps .* weights', 1);
% 存储预测结果
multi_step_predictions(step, :) = prediction .* sigma + mu;
% 更新窗口
current_window = [current_window(2:end, :); prediction];
end
end
6. 实际应用中的注意事项
6.1 计算效率优化
KNN算法在大型数据集上可能计算量很大,可以考虑以下优化:
- KD树或球树:实现更高效的邻居搜索
- 降维技术:对高维时间序列使用PCA等降维方法
- 并行计算:利用Matlab的并行计算工具箱
matlab复制% KD树实现的示例
function knn_model = build_kdtree(X_train)
% 使用第三方KD树实现或自己实现
knn_model = KDTreeSearcher(X_train);
end
function [neighbors_indices, neighbors_distances] = kdtree_search(knn_model, X_test, k)
[neighbors_indices, neighbors_distances] = knnsearch(knn_model, X_test, 'K', k);
end
6.2 参数选择经验
基于实际项目经验,以下参数选择策略通常有效:
- 窗口大小(W):通常选择1-2个周期长度。对于日周期数据,24可能是个好起点
- K值:从5开始尝试,不超过训练样本数的平方根
- 距离度量:简单周期数据用欧氏距离,复杂模式考虑DTW
- 预测步长(horizon):根据实际需求确定,但注意预测误差会随步长增加而累积
6.3 常见问题排查
-
预测结果平坦:
- 检查距离度量是否合理
- 尝试增加窗口大小
- 检查数据是否过度平滑
-
计算速度过慢:
- 减少特征维度
- 使用近似最近邻算法
- 对数据进行下采样
-
预测误差随步长急剧增加:
- 考虑使用迭代多步预测而非直接多步预测
- 增加K值以获得更稳健的预测
- 引入误差修正机制
7. 进阶改进方向
7.1 特征工程增强
改进原始滑动窗口方法,引入更有意义的特征:
matlab复制function enhanced_features = extract_features(windows)
num_samples = size(windows, 1);
num_variables = size(windows, 2) / window_size;
enhanced_features = zeros(num_samples, num_variables * 5); % 每个变量提取5个特征
for i = 1:num_samples
window = reshape(windows(i, :), [], num_variables);
for j = 1:num_variables
ts = window(:, j);
% 统计特征
enhanced_features(i, (j-1)*5+1) = mean(ts);
enhanced_features(i, (j-1)*5+2) = std(ts);
enhanced_features(i, (j-1)*5+3) = max(ts) - min(ts);
% 趋势特征
p = polyfit((1:length(ts))', ts, 1);
enhanced_features(i, (j-1)*5+4) = p(1); % 斜率
% 周期性特征
enhanced_features(i, (j-1)*5+5) = max(abs(fft(ts - mean(ts))));
end
end
end
7.2 集成学习方法
将KNN与其他简单模型集成,提升预测稳定性:
matlab复制function ensemble_predictions = ensemble_predict(X_train, Y_train, X_test, models)
num_models = length(models);
num_test = size(X_test, 1);
num_variables = size(Y_train, 2);
predictions = zeros(num_test, num_variables, num_models);
for i = 1:num_models
if strcmp(models{i}.type, 'knn')
[idx, dist] = find_knn(X_train, X_test, models{i}.k);
predictions(:, :, i) = knn_predict(Y_train, idx, dist);
elseif strcmp(models{i}.type, 'linear')
predictions(:, :, i) = X_test * models{i}.coefficients;
end
end
ensemble_predictions = mean(predictions, 3);
end
7.3 实时预测系统设计
构建一个可以实时更新的预测系统:
matlab复制classdef RealTimeKNNPredictor
properties
window_size
horizon
k
X_train
Y_train
data_buffer
normalization_params
end
methods
function obj = RealTimeKNNPredictor(initial_data, window_size, horizon, k)
obj.window_size = window_size;
obj.horizon = horizon;
obj.k = k;
[normalized_data, mu, sigma] = normalize_data(initial_data);
obj.normalization_params.mu = mu;
obj.normalization_params.sigma = sigma;
[obj.X_train, obj.Y_train] = create_sliding_windows(normalized_data, window_size, horizon);
obj.data_buffer = initial_data(end-window_size+1:end, :);
end
function [obj, prediction] = update(obj, new_data)
% 更新数据缓冲区
obj.data_buffer = [obj.data_buffer(2:end, :); new_data];
% 标准化当前窗口
current_window = (obj.data_buffer - obj.normalization_params.mu) ./ obj.normalization_params.sigma;
current_window_flat = current_window(:)';
% 进行预测
[idx, dist] = find_knn(obj.X_train, current_window_flat, obj.k);
normalized_prediction = knn_predict(obj.Y_train, idx, dist);
% 反标准化
prediction = normalized_prediction .* obj.normalization_params.sigma + obj.normalization_params.mu;
% 可选:更新训练集
% [obj.X_train, obj.Y_train] = update_training_set(obj.X_train, obj.Y_train, ...);
end
end
end
在实际项目中,我发现KNN对于具有明显周期性且噪声较低的时间序列数据表现最佳。当数据中存在突变或异常值时,预测结果可能会受到显著影响。一个实用的技巧是对输入数据进行平滑处理,或者在计算距离时使用更鲁棒的度量方法,如Huber损失。
