使用 .NET 5 體驗大數據和機器學習

翻譯:精緻碼農-王亮
原文://dwz.win/XnM

.NET 5 旨在提供統一的運行時和框架,使其在各平台都有統一的運行時行為和開發體驗。微軟發佈了與 .NET 協作的大數據(.NET for Spark)和機器學習(ML.NET)工具,這些工具共同提供了富有成效的端到端體驗。在本文中,我們將介紹 .NET for Spark、大數據、ML.NET 和機器學習的基礎知識,我們將研究其 API 和功能,向你展示如何開始構建和消費你自己的 Spark 作業和 ML.NET 模型。

什麼是大數據

大數據是一個幾乎不言自明的行業術語。該術語指的是大型數據集,通常涉及 TB 甚至 PB 級的信息,這些數據集被用作分析的輸入,以揭示數據中的模式和趨勢。大數據與傳統工作負載之間的關鍵區別在於,大數據往往過於龐大、複雜或多變,傳統數據庫和應用程序無法處理。一種流行的數據分類方式被稱為 “3V”(譯註:即3個V,Volume 容量、Velocity 速度、Variety 多樣性)。

大數據解決方案是為適應高容量、處理複雜多樣的數據結構而定製的,並通過批處理(靜態)和流處理(動態)來管理速度。

大多數大數據解決方案都提供了在數據倉庫中存儲數據的方式,數據倉庫通常是一個為快速檢索和為並行處理而優化的分佈式集群。處理大數據往往涉及多個步驟,如下圖所示:

Figure 1: The big data process

.NET 5 開發人員如果需要基於大型數據集進行分析和洞察,可以使用基於流行的大數據解決方案 Apache Spark 的 .NET 實現:.NET for Spark。

.NET for Spark

.NET for Spark 基於 Apache Spark,這是一個用於處理大數據的開源分析引擎。它被設計為在內存中處理大量數據,以提供比其他依賴持久化存儲的解決方案更好的性能。它是一個分佈式系統,並行處理工作負載。它為加載數據、查詢數據、處理數據和輸出數據提供支持。

Apache Spark 支持 Java、Scala、Python、R 和 SQL。微軟創建了 .NET for Spark 以增加對 .NET 的支持。該解決方案提供了免費、開放、跨平台的工具,用於使用 .NET 所支持的語言(如 C#和 F#)構建大數據應用程序,這樣你就可以使用現有的 .NET 庫,同時利用 SparkSQL 等 Spark 特性。

Figure 2: Architecture for .NET for Spark

以下代碼展示了一個小而完整的 .NET for Spark 應用程序,它讀取一個文本文件並按降序輸出字數。

using Microsoft.Spark.Sql;

namespace MySparkApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create a Spark session.
            SparkSession spark = SparkSession.Builder().AppName("word_count_sample").GetOrCreate();

            // Create initial DataFrame.
            DataFrame dataFrame = spark.Read().Text("input.txt");

            // Count words.
            DataFrame words = dataFrame.Select(Functions.Split(Functions.Col("value"), " ").Alias("words"))
                .Select(Functions.Explode(Functions .Col("words"))
                .Alias("word"))
                .GroupBy("word")
                .Count()
                .OrderBy(Functions.Col("count").Desc());

            // Show results.
            words.Show();

            // Stop Spark session.
            spark.Stop();
        }
    }
}

在開發機器上配置 .NET for Spark 需要安裝幾個依賴,包括 Java SDK 和 Apache Spark。你可以在這裡(//aka.ms/go-spark-net)查看手把手的入門指南。

Spark for .NET 可在多種環境中運行,並可部署到雲中運行。可部署目標包括 Azure HDInsight、Azure Synapse、AWS EMR Spark 和 Databricks 等。如果數據作為項目可用的一部分,你可以將其與其他 project 文件一起提交。

大數據通常與機器學習一起使用,以獲得關於數據的洞察。

什麼是機器學習

首先,我們先來介紹一下人工智能和機器學習的基本知識。

人工智能(AI)是指計算機模仿人類智慧和能力,如推理和尋找意義。典型的人工智能技術通常是從規則或邏輯系統開始的。作為一個簡單的例子,想一想這樣的場景:你想把某樣東西分類為「麵包」或「不是麵包」。當你開始時,這似乎是一個簡單的問題,例如「如果它有眼睛,它就不是麵包」。然而,你很快就會開始意識到,有很多不同的特徵可以將某物定性為麵包與非麵包,而且特徵越多,一系列的 if 語句就會越長越複雜,如下圖所示:

Figure 3: Determining 「bread or not bread?」 with AI if statements

從上圖中的例子可以看出,傳統的、基於規則的人工智能技術往往難以擴展。這就是機器學習的作用。機器學習(ML)是人工智能的一個子集,它能在過去的數據中找到模式,並從經驗中學習,以對新數據採取行動。ML 允許計算機在沒有明確的邏輯規則編程的情況下進行預測。因此,當你有一個難以(或不可能)用基於規則的編程解決的問題時,你可以使用 ML。你可以把 ML 看作是 “對不可編程的編程”。

為了用 ML 解決「麵包」與「非麵包」的問題,你提供麵包的例子和非麵包的例子(如下圖所示),而不是實現一長串複雜的 if 語句。你將這些例子傳遞給一個算法,該算法在數據中找到模式,並返回一個模型,然後你可以用這個模型來預測尚未被模型「看到」的圖像是「麵包」還是「不是麵包」。

Figure 4: Determining 「bread or not bread?」 with ML

上圖展示了 AI 與 ML 的另一種思考方式。AI 將規則和數據作為輸入,預期輸出基於這些規則的答案。而 ML 則是將數據和答案作為輸入,輸出可用於對新數據進行歸納的規則。

Figure 5: Artificial intelligence compared to machine learning

AI 將規則和數據作為輸入,並根據這些規則輸出預期的答案。ML 將數據和答案作為輸入,並輸出可用於概括新數據的規則。

ML.NET

微軟在 2019 年 5 月的 Build 上發佈了 ML.NET,這是一個面向.NET 開發人員的開源、跨平台 ML 框架。在過去的九年里,微軟的團隊已經廣泛使用該框架的內部版本來實現流行的 ML 驅動功能;一些例子包括 Dynamics 365 欺詐檢測、PowerPoint 設計理念和 Microsoft Defender 防病毒威脅保護。

ML.NET 允許你在.NET 生態系統中構建、訓練和消費 ML 模型,而不需要 ML 或數據科學的背景。ML.NET 可以在任何.NET 運行的地方運行。Windows、Linux、macOS、on-prem、離線場景(如 WinForms 或 WPF 桌面應用)或任何雲端(如 Azure)中。你可以將 ML.NET 用於各種場景,如表 1 所述。

ML.NET 使用自動機器學習(或稱 AutoML)來自動構建和訓練 ML 模型的過程,以根據提供的場景和數據找到最佳模型。你可以通過 AutoML.NET API 或 ML.NET 工具來使用 ML.NET 的 AutoML,其中包括 Visual Studio 中的 Model Builder 和跨平台的 ML.NET CLI,如圖 6 所示。除了訓練最佳模型外,ML.NET 工具還生成在最終用戶.NET 應用程序中消費模型所需的文件和 C#代碼,該應用程序可以是任何.NET 應用程序(桌面、Web、控制台等)。所有 AutoML 方案都提供了本地訓練選項,圖像分類也允許你利用雲的優勢,使用 Model Builder 中的 Azure ML 進行訓練。

Figure 6: ML.NET tooling is built on top of the AutoML.NET API, which is on top of the ML.NET API.

你可以在 Microsoft Docs 中了解更多關於 ML.NET 的信息,網址是://aka.ms/mlnetdocs

ML 和大數據結合

大數據和 ML 可以很好地結合在一起。讓我們構建一個同時使用 Spark for .NET 和 ML.NET 的管道,以展示大數據和 ML 如何一起工作。Markdown 是一種用於編寫文檔和創建靜態網站的流行語言,它使用的語法不如 HTML 複雜,但提供的格式控制比純文本更多。這是從 .NET 文檔庫中的摘取一段 markdown 文件內容:

---
title: Welcome to .NET
description: Getting started with the .NET
family of technologies.
ms.date: 12/03/2019
ms.custom: "updateeachrelease"
---

# Welcome to .NET

See [Get started with .NET Core](core/get-started.md) to learn how to create .NET Core apps.

Build many types of apps with .NET, such as cloud ,IoT, and games using free cross-platform tools...

破折號之間的部分稱為前頁(front matter),是使用 YAML 描述的有關文檔的元數據。以井號(#)開頭的部分是標題。兩個哈希(##)表示二級標題。「 .NET Core 入門」是一個超鏈接。

我們的目標是處理大量文檔,添加諸如字數和估計的閱讀時間之類的元數據,並將相似的文章自動分組在一起。

這是我們將構建的管道:

  • 為每個文檔建立字數統計;
  • 估計每個文檔的閱讀時間;
  • 根據「 TF-IDF」或「術語頻率/反向文檔頻率」為每個文檔創建前 20 個單詞的列表(這將在後面說明)。

第一步是拉取文檔存儲庫和需引用的應用程序。你可以使用任何包含 Markdown 文件的存儲庫及文件夾結構。本文使用的示例來自 .NET 文檔存儲庫,可從 //aka.ms/dot-net-docs 克隆。

為.NET 和 Spark 準備本地環境之後,可以從//aka.ms/spark-ml-example拉取項目。

解決方案文件夾包含一個批處理命令(在倉庫中有提供),你可以使用該命令來運行所有步驟。

處理 Markdown

DocRepoParser 項目以遞歸方式遍歷存儲庫中的子文件夾,以收集各文檔有關的元數據。Common 項目包含幾個幫助程序類。例如,FilesHelper 用於所有文件 I/O。它跟蹤存儲文件和文件名的位置,並提供諸如為其他項目讀取文件的服務。構造函數需要一個標籤(一個唯一標識工作流的數字)和包含文檔的 repo 或頂級文件夾的路徑。默認情況下,它在用戶的本地應用程序數據文件夾下創建一個文件夾。如有必要,可以將其覆蓋。

MarkdownParser利用 Microsoft.Toolkit.Parsers解析 Markdown 的庫。該庫有兩個任務:首先,它必須提取標題和子標題;其次,它必須提取單詞。Markdown 文件以 “塊 “的形式暴露出來,代表標題、鏈接和其他 Markdown 特徵。塊又包含承載文本的「Inlines」。例如,這段代碼通過迭代行和單元格來解析一個 TableBlock,以找到 Inlines。

case TableBlock table:
    table.Rows.SelectMany(r => r.Cells)
        .SelectMany(c => c.Inlines)
        .ForEach(i => candidate = RecurseInline(i, candidate, words, titles));
        break;

此代碼提取超鏈接的文本部分:

case HyperlinkInline hyper:
    if (!string.IsNullOrWhiteSpace(hyper.Text))
    {
        words.Append(hyper.Text.ExtractWords());
    }
    break;

結果是一個 CSV 文件,如下圖所示:

圖7:生成的CSV文件

第一步只是準備要處理的數據。下一步使用 Spark for .NET 作業確定每個文檔的字數,閱讀時間和前 20 個術語。

構建 Spark Job

SparkWordsProcessor項目用來運行 Spark 作業。雖然該應用程序是一個控制台項目,但它需要 Spark 來運行。runjob.cmd批處理命令將作業提交到正確配置的 Windows 計算機上運行。典型作業的模式是創建一個會話或「應用程序」,執行一些邏輯,然後停止會話。

var spark = SparkSession.Builder()
    .AppName(nameof(SparkWordsProcessor))
    .GetOrCreate();
RunJob();
spark.Stop();

通過將其路徑傳遞給 Spark 會話,可以輕鬆讀取上一步的文件。

var docs = spark.Read().HasHeader().Csv(filesHelper.TempDataFile);
docs.CreateOrReplaceTempView(nameof(docs));
var totalDocs = docs.Count();

docs變量解析為一個DataFrameData Frame 本質上是一個帶有一組列和一個通用接口的表,用於與數據交互,而不管其底層來源是什麼。可以從其他 data frame 中引用一個 data frame。SparkSQL 也可以用來查詢 data frame。你必須創建一個臨時視圖,該視圖為 data frame 提供別名,以便從 SQL 中引用它。通過CreateOrReplaceTempView方法,可以像這樣從 data frame 中查詢行:

SELECT * FROM docs

totalDocs變量檢索文檔中所有行的計數。Spark 提供了一個名為Split的將字符串分解為數組的函數。Explode函數將每個數組項變成一行:

var words = docs.Select(fileCol,
    Functions.Split(nameof(FileDataParse.Words)
    .AsColumn(), " ")
    .Alias(wordList))
    .Select(fileCol, Functions.Explode(wordList.AsColumn())
    .Alias(word));

該查詢為每個單詞或術語生成一行。這個 data frame 是生成術語頻率(TF)或者說每個文檔中每個詞的計數的基礎。

var termFrequency = words
    .GroupBy(fileCol, Functions.Lower(word.AsColumn()).Alias(word))
    .Count()
    .OrderBy(fileCol, count.AsColumn().Desc());

Spark 有內置的模型,可以確定「術語頻率/反向文檔頻率」。在這個例子中,你將手動確定術語頻率來演示它是如何計算的。術語在每個文檔中以特定的頻率出現。一篇關於 wizard 的文檔可能有很高的「wizard」一詞計數。同一篇文檔中,”the “和 “is “這兩個詞的出現次數可能也很高。對我們來說,很明顯,「wizard」這個詞更重要,也提供了更多的語境。另一方面,Spark 必須經過訓練才能識別重要的術語。為了確定什麼是真正重要的,我們將總結文檔頻率(document frequency),或者說一個詞在 repo 中所有文檔中出現的次數。這就是「按不同出現次數分組」:

var documentFrequency = words
    .GroupBy(Functions.Lower(word.AsColumn())
    .Alias(word))
    .Agg(Functions.CountDistinct(fileCol)
    .Alias(docFrequency));

現在是計算的時候了。一個特殊的方程式可以計算出所謂的反向文檔頻率(inverse document frequency),即 IDF。將總文檔的自然對數(加一)輸入方程,然後除以該詞的文檔頻率(加一)。

static double CalculateIdf(int docFrequency, int totalDocuments) =>
    Math.Log(totalDocuments + 1) / (docFrequency + 1);

在所有文檔中出現的詞比出現頻率較低的詞賦值低。例如,給定 1000 個文檔,一個在每個文檔中出現的詞與一個只在少數文檔中出現的詞(約 1 個)相比,IDF 為 0.003。Spark 支持用戶定義的函數,你可以這樣註冊。

spark.Udf().Register<int, int, double>(nameof(CalculateIdf), CalculateIdf);

接下來,你可以使用該函數來計算 data frame 中所有單詞的 IDF:

var idfPrep = documentFrequency.Select(word.AsColumn(),
    docFrequency.AsColumn())
        .WithColumn(total, Functions.Lit(totalDocs))
        .WithColumn(inverseDocFrequency,
            Functions.CallUDF(nameof(CalculateIdf), docFrequency.AsColumn(), total.AsColumn()
        )
    );

使用文檔頻率 data frame,增加兩列。第一列是文檔的單詞總數量,第二列是調用你的 UDF 來計算 IDF。還有一個步驟,就是確定「重要詞」。重要詞是指在所有文檔中不經常出現,但在當前文檔中經常出現的詞,用 TF-IDF 表示,這只是 IDF 和 TF 的產物。考慮「is」的情況,IDF 為 0.002,在文檔中的頻率為 50,而「wizard」的 IDF 為 1,頻率為 10。相比頻率為 10 的「wizard」,「is」的 TF-IDF 計算結果為 0.1。這讓 Spark 對重要性有了更好的概念,而不僅僅是原始字數。

到目前為止,你已經使用代碼來定義 data frame。讓我們嘗試一下 SparkSQL。為了計算 TF-IDF,你將文檔頻率 data frame 與反向文檔頻率 data frame 連接起來,並創建一個名為termFreq_inverseDocFreq的新列。下面是 SparkSQL:

var idfJoin = spark.Sql($"SELECT t.File, d.word, d.{docFrequency}, d.{inverseDocFrequency}, t.count, d.{inverseDocFrequency} * t.count as {termFreq_inverseDocFreq} from {nameof(documentFrequency)} d inner join {nameof(termFrequency)} t on t.word = d.word");

探索代碼,看看最後的步驟是如何實現的。這些步驟包括:

到目前為止所描述的所有步驟都為 Spark 提供了一個模板或定義。像 LINQ 查詢一樣,實際的處理在結果被具體化之前不會發生(比如計算出總文檔數時)。最後一步調用 Collect 來處理和返回結果,並將其寫入另一個 CSV。然後,你可以使用新文件作為 ML 模型的輸入,下圖是該文件的一部分:

圖8:已準備好進行ML訓練的已處理元數據。

Spark for .NET 使你能夠查詢和塑造數據。你在同一個數據源上建立了多個 data frame,然後添加它們以獲得關於重要術語、字數和閱讀時間的洞察。下一步是應用 ML 來自動生成類別。

預測類別

最後一步是對文檔進行分類。DocMLCategorization項目包含了 ML.NET 的Microsoft.ML包。雖然 Spark 使用的是 data frame,但 data view 在 ML.NET 中提供了類似的概念。

這個例子為 ML.NET 使用了一個單獨的項目,這樣就可以將模型作為一個獨立的步驟進行訓練。對於許多場景,可以直接從你的.NET for Spark 項目中引用 ML.NET,並將 ML 作為同一工作的一部分來執行。

首先,你必須對類進行標記,以便 ML.NET 知道源數據中的哪些列映射到類中的屬性。在FileData 類使用 LoadColumn 註解,就像這樣:

[LoadColumn(0)]
public string File { get; set; }

[LoadColumn(1)]
public string Title { get; set; }

然後,你可以為模型創建上下文,並從上一步中生成的文件中加載 data view:

var context = new MLContext(seed: 0);
var dataToTrain = context.Data
    .LoadFromTextFile<FileData>(path: filesHelper.ModelTrainingFile, hasHeader: true, allowQuoting: true, separatorChar: ',');

ML 算法對數字的處理效果最好,所以文檔中的文本必須轉換為數字向量。ML.NET 為此提供了FeaturizeText方法。在一個步驟中,模型分別:

  • 檢測語言
  • 將文本標記為單個單詞或標記
  • 規範化文本,以便對單詞的變體進行標準化和大小寫相似化
  • 將這些術語轉換為一致的數值或準備處理的「特徵向量」

以下代碼將列轉換為特徵,然後創建一個結合了多個特徵的「Features」列。

var pipeline = context.Transforms.Text.FeaturizeText(
    nameof(FileData.Title).Featurized(),
    nameof(FileData.Title)).Append(context.Transforms.Text.FeaturizeText(nameof(FileData.Subtitle1).Featurized(),
    nameof(FileData.Subtitle1))).Append(context.Transforms.Text.FeaturizeText(nameof(FileData.Subtitle2).Featurized(),
    nameof(FileData.Subtitle2))).Append(context.Transforms.Text.FeaturizeText(nameof(FileData.Subtitle3).Featurized(),
    nameof(FileData.Subtitle3))).Append(context.Transforms.Text.FeaturizeText(nameof(FileData.Subtitle4).Featurized(),
    nameof(FileData.Subtitle4))).Append(context.Transforms.Text.FeaturizeText(nameof(FileData.Subtitle5).Featurized(),
    nameof(FileData.Subtitle5))).Append(context.Transforms.Text.FeaturizeText(nameof(FileData.Top20Words).Featurized(),
    nameof(FileData.Top20Words))).Append(context.Transforms.Concatenate(features, nameof(FileData.Title).Featurized(),
    nameof(FileData.Subtitle1).Featurized(),
    nameof(FileData.Subtitle2).Featurized(),
    nameof(FileData.Subtitle3).Featurized(),
    nameof(FileData.Subtitle4).Featurized(),
    nameof(FileData.Subtitle5).Featurized(),
    nameof(FileData.Top20Words).Featurized())
);

此時,數據已經為訓練模型做了適當的準備。訓練是無監督的,這意味着它必須用一個例子來推斷信息。你沒有將樣本類別輸入到模型中,所以算法必須通過分析特徵如何聚類來找出數據的相互關聯。你將使用k-means 聚類算法。該算法使用特徵計算文檔之間的「距離」,然後圍繞分組後的文檔「繪製」邊界。該算法涉及隨機化,因此兩次運行結果會是不相同的。主要的挑戰是確定訓練的最佳聚類大小。不同的文檔集最好有不同的最佳類別數,但算法需要你在訓練前輸入類別數。

代碼在 2 到 20 個簇之間迭代,以確定最佳大小。對於每次運行,它都會獲取特徵數據並應用算法或訓練器。然後,它根據預測模型對現有數據進行轉換。對結果進行評估,以確定每個簇中文檔的平均距離,並選擇平均距離最小的結果。

var options = new KMeansTrainer.Options
{
    FeatureColumnName = features,
    NumberOfClusters = categories,
};

var clusterPipeline = pipeline.Append(context.Clustering.Trainers.KMeans(options));
var model = clusterPipeline.Fit(dataToTrain);
var predictions = model.Transform(dataToTrain);
var metrics = context.Clustering.Evaluate(predictions);
distances.Add(categories, metrics.AverageDistance);

經過培訓和評估後,你可以保存最佳模型,並使用它對數據集進行預測。將生成一個輸出文件以及一個摘要,該摘要顯示有關每個類別的一些元數據並在下面列出標題。標題只是幾個功能之一,因此有時需要仔細研究細節才能使類別有意義。在本地測試中,教程之類的文檔歸於一組,API 文檔歸於另一組,而例外歸於它們自己的組。

ML zip 文件可與 Prediction Engine 一起用於其他項目中的新數據。

機器學習模型另存為單個 zip 文件。該文件可以包含在其他項目中,與 Prediction Engine 一起使用以對新數據進行預測。例如,你可以創建一個 WPF 應用程序,該應用程序允許用戶瀏覽目錄,然後加載並使用經過訓練的模型對文檔進行分類,而無需先對其進行訓練。

下一步是什麼

Spark for .NET 計劃與.NET 5 同時在 GA(譯註:GA=General Availability,正式發佈的版本)發佈。請訪問 //aka.ms/spark-net-roadmap 閱讀路線圖和推出功能的計劃。(譯註:.NET 5 正式發佈時間已過,Spark for .NET 已隨 .NET 5 正式發佈)

本文着重於本地開發體驗,為了充分利用大數據的力量,你可以將 Spark 作業提交到雲中。有各種各樣的雲主機可以容納 PB 級數據,並為你的工作負載提供數十個核的計算能力。Azure Synapse Analytics 是一項 Azure 服務,旨在承載大量數據,提供用於運行大數據作業的群集,並允許通過基於圖表的儀錶盤進行交互式探索。若要了解如何將 Spark for .NET 作業提交到 Azure Synapse,請閱讀官方文檔(//aka.ms/spark-net-synapse)。

下面這張表列舉了 ML.NET 機器學習的常見任務和場景:

任務 示例場景
分類(基於文本) Classification 將郵件信息分類為垃圾郵件或非垃圾郵件,或根據內容將調查評論分為不同的組別。
回歸 Regression 根據二手車的品牌、型號、里程數來預測二手車的價格,或者根據廣告預算來預測產品的銷量。
預測 Forecasting 根據過去的銷售情況來預測未來產品的銷售情況,或天氣預報。
異常檢測 Anomaly detection 檢測產品在一段時間內的銷售高峰或檢測斷電情況。
排名 Ranking 預測搜索引擎結果的最佳顯示順序,或為用戶的新聞排序。
聚類 Clustering 對客戶進行細分。
推薦 Recommendation 根據用戶之前看的電影向用戶推薦電影,或者推薦經常一起購買的產品。
圖像分類 Image classification 對機器零件的圖像進行分類。
對象檢測 Object detection 檢測汽車圖像上的車牌。