使用 .NET 5 體驗大資料和機器學習

精緻碼農發表於2020-11-16

翻譯:精緻碼農-王亮
原文:http://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。你可以在這裡(https://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 的資訊,網址是:https://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 文件儲存庫,可從 https://aka.ms/dot-net-docs 克隆。

為.NET 和 Spark 準備本地環境之後,可以從https://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,正式釋出的版本)釋出。請訪問 https://aka.ms/spark-net-roadmap 閱讀路線圖和推出功能的計劃。(譯註:.NET 5 正式釋出時間已過,Spark for .NET 已隨 .NET 5 正式釋出)

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

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

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

相關文章