教程:使用时序分析和 ML.NET 预测自行车租赁服务需求
了解如何通过 ML.NET 对 SQL Server 数据库中存储的数据进行单变量时序分析,以预测自行车租赁服务需求。
在本教程中,你将了解:
- 了解问题
- 从数据库加载数据
- 创建预测模型
- 评估预测模型
- 保存预测模型
- 使用预测模型
先决条件
- 已安装“.NET 桌面开发”工作负载的 Visual Studio 2022。
时序预测示例概述
此示例为 C# .NET Core 控制台应用程序,它使用单变量时序分析算法(称为单谱分析)来预测自行车租赁需求。 此示例的代码可以在 GitHub 上的 dotnet/machinelearning-samples 存储库找到。
了解问题
为了实现高效运营,其中库存管理的作用不可或缺。 产品库存过多意味着产品积压,无法产生收入。 产品库存过少会损失销售额,导致客户转而购买竞争对手的产品。 因此,一个永恒的问题就是:保有多少库存才最合适呢? 借助时序分析,可通过查看历史数据、识别模式并使用此信息来预测未来某个时间的值,从而帮助找到这些问题的答案。
此教程使用的数据分析技术为单变量时序分析。 单变量时序分析可按照特定间隔(如月销售额)查看一个时段内的单个数值观测。
本教程中使用的算法是单谱分析 (SSA)。 SSA 会将时序分解为一组主要成分, 可以将这些成分解释为信号的组成部分,对应于趋势、噪音、季节性及许多其他的因素。 然后重新构建这些成分,并用来预测未来某个时间的值。
创建控制台应用程序
创建一个名为“BikeDemandForecasting”的 C# 控制台应用程序。 单击“下一步”按钮。
选择 .NET 6 作为要使用的框架。 单击“创建” 按钮。
安装 Microsoft.ML 版本 NuGet 包
注意
除非另有说明,否则本示例使用前面提到的 NuGet 包的最新稳定版本。
- 在“解决方案资源管理器”中,右键单击项目,然后选择“管理 NuGet 包” 。
- 选择“nuget.org”作为“包源”,选择“浏览”选项卡,再搜索“Microsoft.ML”。
- 选中“包括预发行版”复选框。
- 选择“安装”按钮。
- 选择“预览更改”对话框中的“确定”按钮;如果同意所列包的许可条款,请选择“接受许可”对话框中的“我接受”按钮。
- 针对 System.Data.SqlClient 和 Microsoft.ML.TimeSeries 重复上述步骤 。
准备和了解数据
- 创建一个名为“Data”的目录。
- 下载 DailyDemand.mdf 数据库文件并将其保存到“Data”目录中。
注意
此教程使用的数据来自 UCI 自行车共享数据集。 作者 Fanaee-T,Hadi 和 Gama, Joao,“事件标签结合集合探测器和背景知识”,人工智能进展 (2013):1-15 页,Springer Berlin Heidelberg,网页链接。
原始数据集包含与季节和天气相对应的若干列。 为了简洁起见,并且由于本教程使用的算法仅需要单个数值列中的值,因此,已将原始数据集精简为仅包括以下列:
- dteday:观测日期。
- year:观测年份编码(0=2011,1=2012)。
- cnt:观测日当天自行车租赁总数。
原始数据集映射到 SQL Server 数据库中具有以下架构的数据库表。
CREATE TABLE [Rentals] (
[RentalDate] DATE NOT NULL,
[Year] INT NOT NULL,
[TotalRentals] INT NOT NULL
);
以下是数据示例:
RentalDate | 年 | TotalRentals |
---|---|---|
1/1/2011 | 0 | 985 |
1/2/2011 | 0 | 801 |
1/3/2011 | 0 | 1349 |
创建输入和输出类
打开 Program.cs 文件,将现有
using
语句替换为以下内容:using Microsoft.ML; using Microsoft.ML.Data; using Microsoft.ML.Transforms.TimeSeries; using System.Data.SqlClient;
创建
ModelInput
类。 在Program
类下面,添加以下代码。public class ModelInput { public DateTime RentalDate { get; set; } public float Year { get; set; } public float TotalRentals { get; set; } }
ModelInput
类包含以下列:- RentalDate:观测日期。
- Year:观测年份编码(0=2011,1=2012)。
- TotalRentals:观测日当天自行车租赁总数。
在新建的
ModelOutput
类的下面,创建ModelInput
类。public class ModelOutput { public float[] ForecastedRentals { get; set; } public float[] LowerBoundRentals { get; set; } public float[] UpperBoundRentals { get; set; } }
ModelOutput
类包含以下列:- ForecastedRentals:预测时段内的预测值。
- LowerBoundRentals:预测时段内的最低预测值。
- UpperBoundRentals:预测时段内的最高预测值。
定义路径并初始化变量
在 using 语句下,定义变量,用于存储数据位置、连接字符串,以及保存已训练模型的位置。
string rootDir = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../")); string dbFilePath = Path.Combine(rootDir, "Data", "DailyDemand.mdf"); string modelPath = Path.Combine(rootDir, "MLModel.zip"); var connectionString = $"Data Source=(LocalDB)\\MSSQLLocalDB;AttachDbFilename={dbFilePath};Integrated Security=True;Connect Timeout=30;";
通过在定义路径后添加以下行,使用新的
MLContext
实例初始化mlContext
变量。MLContext mlContext = new MLContext();
执行所有 ML.NET 操作都是从
MLContext
类开始,初始化 mlContext 将创建一个新的 ML.NET 环境,可在模型创建工作流对象之间共享该环境。 从概念上讲,它与实体框架中的DBContext
类似。
加载数据
创建
DatabaseLoader
,用于加载ModelInput
类型的记录。DatabaseLoader loader = mlContext.Data.CreateDatabaseLoader<ModelInput>();
定义查询,以从数据库加载数据。
string query = "SELECT RentalDate, CAST(Year as REAL) as Year, CAST(TotalRentals as REAL) as TotalRentals FROM Rentals";
ML.NET 算法要求数据是
Single
类型。 因此,必须将来自数据库的非Real
类型的数值(单精度浮点值)转换为Real
。数据库中的
Year
和TotalRental
列都是整数类型。 使用CAST
内置函数将它们都转换为Real
。创建
DatabaseSource
以连接到数据库,并执行查询。DatabaseSource dbSource = new DatabaseSource(SqlClientFactory.Instance, connectionString, query);
将数据加载到
IDataView
中。IDataView dataView = loader.Load(dbSource);
此数据集包含两年的重要数据。 第一年的数据仅用于培训,第二年的数据用于将实际值与模型生成的预测进行比较。 使用
FilterRowsByColumn
转换筛选数据。IDataView firstYearData = mlContext.Data.FilterRowsByColumn(dataView, "Year", upperBound: 1); IDataView secondYearData = mlContext.Data.FilterRowsByColumn(dataView, "Year", lowerBound: 1);
对于第一年,通过将
upperBound
参数设置为 1 来仅选择Year
列中小于 1 的值。 相反,对于第二年,通过将lowerBound
参数设置为 1 来仅选择大于或等于 1 的值。
定义时序分析管道
定义使用 SsaForecastingEstimator 预测时序数据集中的值的管道。
var forecastingPipeline = mlContext.Forecasting.ForecastBySsa( outputColumnName: "ForecastedRentals", inputColumnName: "TotalRentals", windowSize: 7, seriesLength: 30, trainSize: 365, horizon: 7, confidenceLevel: 0.95f, confidenceLowerBoundColumn: "LowerBoundRentals", confidenceUpperBoundColumn: "UpperBoundRentals");
forecastingPipeline
在第一年数据中获取 365 个数据点,并按seriesLength
参数指定的间隔从时序数据集采样或将其分为 30 天(每月)的间隔。 以一周或 7 天为一个时段分析各个样本。 确定下一个时段的预测值时,使用前面 7 天的值进行预测。 根据horizon
参数的定义,该模型设置为预测将来的 7 个时段。 由于预测属于合理猜测,它不总是完全准确。 因此,最好了解上限和下限定义的最佳和最坏情况下的范围值。 在本案例中,设置的上下限可信度为 95%。 可信度可以相应地提高或降低。 值越高,上限和下限之间的范围越大,以便达到所需的可信度。使用
Fit
方法培训模型,使数据适用于前面定义的forecastingPipeline
。SsaForecastingTransformer forecaster = forecastingPipeline.Fit(firstYearData);
评估模型
通过预测下一年的数据并将其与实际值进行比较,评估模型的执行情况。
在 Program.cs 文件底部创建名为
Evaluate
的新实用工具方法。Evaluate(IDataView testData, ITransformer model, MLContext mlContext) { }
在
Evaluate
方法中,通过结合使用Transform
方法和培训模型,预测第二年的数据。IDataView predictions = model.Transform(testData);
使用
CreateEnumerable
方法,从数据中获取实际值。IEnumerable<float> actual = mlContext.Data.CreateEnumerable<ModelInput>(testData, true) .Select(observed => observed.TotalRentals);
使用
CreateEnumerable
方法获取预测值。IEnumerable<float> forecast = mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true) .Select(prediction => prediction.ForecastedRentals[0]);
计算实际值和预测值之间的差值(通常称为“误差”)。
var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);
通过计算平均绝对误差和均方根误差值测量性能。
var MAE = metrics.Average(error => Math.Abs(error)); // Mean Absolute Error var RMSE = Math.Sqrt(metrics.Average(error => Math.Pow(error, 2))); // Root Mean Squared Error
使用以下指标来评估性能:
- 平均绝对误差:度量预测与实际值之间的接近程度。 此值介于 0 到无限大之间。 越接近 0,模型的质量越好。
- 均方根误差:汇总模型中的错误。 此值介于 0 到无限大之间。 越接近 0,模型的质量越好。
将指标输出到控制台。
Console.WriteLine("Evaluation Metrics"); Console.WriteLine("---------------------"); Console.WriteLine($"Mean Absolute Error: {MAE:F3}"); Console.WriteLine($"Root Mean Squared Error: {RMSE:F3}\n");
在调用
Fit()
方法下方调用Evaluate
方法。Evaluate(secondYearData, forecaster, mlContext);
保存模型
如果对模型满意,则保存它,以便以后用于其他应用程序。
在
Evaluate()
方法下面,创建TimeSeriesPredictionEngine
。TimeSeriesPredictionEngine
是进行单个预测的一个便捷方法。var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);
将此模型保存到由先前定义的
modelPath
变量指定的名为MLModel.zip
的文件。 使用Checkpoint
方法保存模型。forecastEngine.CheckPoint(mlContext, modelPath);
使用模型预测需求
在
Evaluate
方法下面,创建一个名为Forecast
的新实用方法。void Forecast(IDataView testData, int horizon, TimeSeriesPredictionEngine<ModelInput, ModelOutput> forecaster, MLContext mlContext) { }
在
Forecast
方法中,使用Predict
方法预测接下来的 7 天的租赁数量。ModelOutput forecast = forecaster.Predict();
排列 7 个时段的实际值和预测值。
IEnumerable<string> forecastOutput = mlContext.Data.CreateEnumerable<ModelInput>(testData, reuseRowObject: false) .Take(horizon) .Select((ModelInput rental, int index) => { string rentalDate = rental.RentalDate.ToShortDateString(); float actualRentals = rental.TotalRentals; float lowerEstimate = Math.Max(0, forecast.LowerBoundRentals[index]); float estimate = forecast.ForecastedRentals[index]; float upperEstimate = forecast.UpperBoundRentals[index]; return $"Date: {rentalDate}\n" + $"Actual Rentals: {actualRentals}\n" + $"Lower Estimate: {lowerEstimate}\n" + $"Forecast: {estimate}\n" + $"Upper Estimate: {upperEstimate}\n"; });
循环访问预测输出,并在控制台上显示它。
Console.WriteLine("Rental Forecast"); Console.WriteLine("---------------------"); foreach (var prediction in forecastOutput) { Console.WriteLine(prediction); }
运行此应用程序
在调用
Checkpoint()
方法下方调用Forecast
方法。Forecast(secondYearData, 7, forecastEngine, mlContext);
运行该应用程序。 控制台应显示类似以下内容的输出。 为简洁起见,输出已进行压缩。
Evaluation Metrics --------------------- Mean Absolute Error: 726.416 Root Mean Squared Error: 987.658 Rental Forecast --------------------- Date: 1/1/2012 Actual Rentals: 2294 Lower Estimate: 1197.842 Forecast: 2334.443 Upper Estimate: 3471.044 Date: 1/2/2012 Actual Rentals: 1951 Lower Estimate: 1148.412 Forecast: 2360.861 Upper Estimate: 3573.309
通过观测实际值和预测值,获得以下关系:
尽管预测值并不能预测准确的租赁数,但它们缩小了值的范围,企业可以通过它们优化资源利用。
祝贺你! 你已成功生成用于预测自行车租赁需求的时序机器学习模型。
可以在 dotnet/machinelearning-samples 存储库中找到本教程的源代码。