教程:使用时序分析和 ML.NET 预测自行车租赁服务需求

了解如何通过 ML.NET 对 SQL Server 数据库中存储的数据进行单变量时序分析,以预测自行车租赁服务需求。

在本教程中,你将了解:

  • 了解问题
  • 从数据库加载数据
  • 创建预测模型
  • 评估预测模型
  • 保存预测模型
  • 使用预测模型

先决条件

时序预测示例概述

此示例为 C# .NET Core 控制台应用程序,它使用单变量时序分析算法(称为单谱分析)来预测自行车租赁需求。 此示例的代码可以在 GitHub 上的 dotnet/machinelearning-samples 存储库找到。

了解问题

为了实现高效运营,其中库存管理的作用不可或缺。 产品库存过多意味着产品积压,无法产生收入。 产品库存过少会损失销售额,导致客户转而购买竞争对手的产品。 因此,一个永恒的问题就是:保有多少库存才最合适呢? 借助时序分析,可通过查看历史数据、识别模式并使用此信息来预测未来某个时间的值,从而帮助找到这些问题的答案。

此教程使用的数据分析技术为单变量时序分析。 单变量时序分析可按照特定间隔(如月销售额)查看一个时段内的单个数值观测。

本教程中使用的算法是单谱分析 (SSA)。 SSA 会将时序分解为一组主要成分, 可以将这些成分解释为信号的组成部分,对应于趋势、噪音、季节性及许多其他的因素。 然后重新构建这些成分,并用来预测未来某个时间的值。

创建控制台应用程序

  1. 创建一个名为“BikeDemandForecasting”的 C# 控制台应用程序。 单击“下一步”按钮。

  2. 选择 .NET 6 作为要使用的框架。 单击“创建” 按钮。

  3. 安装 Microsoft.ML 版本 NuGet 包

    注意

    除非另有说明,否则本示例使用前面提到的 NuGet 包的最新稳定版本。

    1. 在“解决方案资源管理器”中,右键单击项目,然后选择“管理 NuGet 包” 。
    2. 选择“nuget.org”作为“包源”,选择“浏览”选项卡,再搜索“Microsoft.ML”。
    3. 选中“包括预发行版”复选框。
    4. 选择“安装”按钮。
    5. 选择“预览更改”对话框中的“确定”按钮;如果同意所列包的许可条款,请选择“接受许可”对话框中的“我接受”按钮。
    6. 针对 System.Data.SqlClient 和 Microsoft.ML.TimeSeries 重复上述步骤 。

准备和了解数据

  1. 创建一个名为“Data”的目录。
  2. 下载 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

创建输入和输出类

  1. 打开 Program.cs 文件,将现有 using 语句替换为以下内容:

    using Microsoft.ML;
    using Microsoft.ML.Data;
    using Microsoft.ML.Transforms.TimeSeries;
    using System.Data.SqlClient;
    
  2. 创建 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:观测日当天自行车租赁总数。
  3. 在新建的 ModelOutput 类的下面,创建 ModelInput 类。

    public class ModelOutput
    {
        public float[] ForecastedRentals { get; set; }
    
        public float[] LowerBoundRentals { get; set; }
    
        public float[] UpperBoundRentals { get; set; }
    }
    

    ModelOutput 类包含以下列:

    • ForecastedRentals:预测时段内的预测值。
    • LowerBoundRentals:预测时段内的最低预测值。
    • UpperBoundRentals:预测时段内的最高预测值。

定义路径并初始化变量

  1. 在 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;";
    
  2. 通过在定义路径后添加以下行,使用新的 MLContext 实例初始化 mlContext 变量。

    MLContext mlContext = new MLContext();
    

    执行所有 ML.NET 操作都是从 MLContext 类开始,初始化 mlContext 将创建一个新的 ML.NET 环境,可在模型创建工作流对象之间共享该环境。 从概念上讲,它与实体框架中的 DBContext 类似。

加载数据

  1. 创建 DatabaseLoader,用于加载 ModelInput 类型的记录。

    DatabaseLoader loader = mlContext.Data.CreateDatabaseLoader<ModelInput>();
    
  2. 定义查询,以从数据库加载数据。

    string query = "SELECT RentalDate, CAST(Year as REAL) as Year, CAST(TotalRentals as REAL) as TotalRentals FROM Rentals";
    

    ML.NET 算法要求数据是 Single 类型。 因此,必须将来自数据库的非 Real 类型的数值(单精度浮点值)转换为 Real

    数据库中的 YearTotalRental 列都是整数类型。 使用 CAST 内置函数将它们都转换为 Real

  3. 创建 DatabaseSource 以连接到数据库,并执行查询。

    DatabaseSource dbSource = new DatabaseSource(SqlClientFactory.Instance,
                                    connectionString,
                                    query);
    
  4. 将数据加载到 IDataView 中。

    IDataView dataView = loader.Load(dbSource);
    
  5. 此数据集包含两年的重要数据。 第一年的数据仅用于培训,第二年的数据用于将实际值与模型生成的预测进行比较。 使用 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 的值。

定义时序分析管道

  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%。 可信度可以相应地提高或降低。 值越高,上限和下限之间的范围越大,以便达到所需的可信度。

  2. 使用 Fit 方法培训模型,使数据适用于前面定义的 forecastingPipeline

    SsaForecastingTransformer forecaster = forecastingPipeline.Fit(firstYearData);
    

评估模型

通过预测下一年的数据并将其与实际值进行比较,评估模型的执行情况。

  1. 在 Program.cs 文件底部创建名为 Evaluate 的新实用工具方法。

    Evaluate(IDataView testData, ITransformer model, MLContext mlContext)
    {
    
    }
    
  2. Evaluate 方法中,通过结合使用 Transform 方法和培训模型,预测第二年的数据。

    IDataView predictions = model.Transform(testData);
    
  3. 使用 CreateEnumerable 方法,从数据中获取实际值。

    IEnumerable<float> actual =
        mlContext.Data.CreateEnumerable<ModelInput>(testData, true)
            .Select(observed => observed.TotalRentals);
    
  4. 使用 CreateEnumerable 方法获取预测值。

    IEnumerable<float> forecast =
        mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true)
            .Select(prediction => prediction.ForecastedRentals[0]);
    
  5. 计算实际值和预测值之间的差值(通常称为“误差”)。

    var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);
    
  6. 通过计算平均绝对误差和均方根误差值测量性能。

    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,模型的质量越好。
  7. 将指标输出到控制台。

    Console.WriteLine("Evaluation Metrics");
    Console.WriteLine("---------------------");
    Console.WriteLine($"Mean Absolute Error: {MAE:F3}");
    Console.WriteLine($"Root Mean Squared Error: {RMSE:F3}\n");
    
  8. 在调用 Fit() 方法下方调用 Evaluate 方法。

    Evaluate(secondYearData, forecaster, mlContext);
    

保存模型

如果对模型满意,则保存它,以便以后用于其他应用程序。

  1. Evaluate() 方法下面,创建 TimeSeriesPredictionEngineTimeSeriesPredictionEngine 是进行单个预测的一个便捷方法。

    var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);
    
  2. 将此模型保存到由先前定义的 modelPath 变量指定的名为 MLModel.zip 的文件。 使用 Checkpoint 方法保存模型。

    forecastEngine.CheckPoint(mlContext, modelPath);
    

使用模型预测需求

  1. Evaluate 方法下面,创建一个名为 Forecast 的新实用方法。

    void Forecast(IDataView testData, int horizon, TimeSeriesPredictionEngine<ModelInput, ModelOutput> forecaster, MLContext mlContext)
    {
    
    }
    
  2. Forecast 方法中,使用 Predict 方法预测接下来的 7 天的租赁数量。

    ModelOutput forecast = forecaster.Predict();
    
  3. 排列 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";
            });
    
  4. 循环访问预测输出,并在控制台上显示它。

    Console.WriteLine("Rental Forecast");
    Console.WriteLine("---------------------");
    foreach (var prediction in forecastOutput)
    {
        Console.WriteLine(prediction);
    }
    

运行此应用程序

  1. 在调用 Checkpoint() 方法下方调用 Forecast 方法。

    Forecast(secondYearData, 7, forecastEngine, mlContext);
    
  2. 运行该应用程序。 控制台应显示类似以下内容的输出。 为简洁起见,输出已进行压缩。

    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 存储库中找到本教程的源代码。

后续步骤