半年前寫過一篇類似的文章,題目是:《在.NET中使用機器學習API(ML.NET)實現化學分子式資料格式的判定》,在該文中,我介紹了化學分子式資料格式的基本知識,同時給出了一個案例,展示瞭如何在.NET/.NET Core中,使用微軟開源的ML.NET框架,透過機器學習,實現化學分子式資料格式的預測。
時隔半年,ML.NET有了很大的發展。在閱讀我之前那篇文章的時候,或許還會對給出的案例程式碼有些疑問,ML.NET經過幾個版本的更新之後,API的設計變得更為合理易用,所開放的介面也越來越多(比如,新版本的ML.NET中,對機器學習引擎的OutputSchema進行了完全開放,開發者可以根據自己的需要進行呼叫),因此,本文就再一次回到這個話題併進行更為詳細的介紹,用新版本的ML.NET重新實現化學分子式資料格式的判定。
有關化學分子式的相關知識,在這裡也就不多說了,直接看程式碼實現部分。
準備資料
我們的資料仍然是一個CSV檔案,透過逗號分隔,檔案包含兩個欄位:結構式資料(ChemicalStructure),以及該結構式資料的型別(Type),以下是這個檔案的部分片段,註意,在這個檔案中,我們沒有定義CSV頭,不過這不重要,只要記得在後面的程式碼實現中,將這個設定體現出來就可以了。
[O-]C(CCCCCCCCCCCCCCCCC)=O.[Na+],SMILES O=C(C1)N(C2[C@@]3(CC4)[C@](N4C5)([H])C[C@@]6([H])C5=CCOC1[C@]62[H])C7=C3C=CC=C7.O[N+]([O-])=O,SMILES O=C1CC2C(C3[C@]45C(C=CC=C6)=C6N31)C(CC4N(CC5)C7)C7=CCO2.OS(O)(=O)=O.O=C8CC9C(C%10[C@@]%11%12C(C=CC=C%13)=C%13N%108)C(CC%11N(CC%12)C%14)C%14=CCO9,SMILES C=CC1=CC=CC=C1,SMILES N=C(OC)CCCCCCC(OC)=N.Cl.Cl,SMILES NC(CCC(N)=O)=O,SMILES O=C(O)C1(N(CCOC)CCOC)CCC(C)CC1,SMILES CN(C)C(C)CC(C1=CC=CC=C1)(C(CC)=O)C2=CC=CC=C2,SMILES NCC1(CCC(CCC)CC1)N(C)CC2=COC=C2,SMILES AAADceByOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAAAAAACBThgAYCCAMABAAIAACQCAAAAAAAAAAAAAEIAAACABQAgAAHAAAFIAAQAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==,BASE64_CDX AAADceByOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAACAAACBThgAYCCAMABgAIAACQCAAAAAAAAAAAAAEIAAACABQAgAAHQAAFIAAQAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==,BASE64_CDX AAADccBCIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHgAQCAAACBThgAYCAABAAgAAAAAAAAAAAAAAAAAAAIAAAAACEAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==,BASE64_CDX AAADccBjgAAAAAAAAAAAAAAAAAAAAWAAAAAsAAAAAAAAAFgB+AAAHAAQAAAACAjBFwQH8L9MEACgAQZhZACAgC0REKABUCAoVBCASABASEAUBAgIAALAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==,BASE64_CDX AAADceB7uAAAAAAAAAAAAAAAAAAAAAAAAAAwQIAAAAAAAACBAAAAHgAQCAAADCjBmAYxyIPAAgCoAiXS/ACCAAElAgAJiIGIZMiKYDLA1bGUYQhslgLYyce8rwCeCAAAAAAAAAAQAAAAAAAAAAAAAAAAAA==,BASE64_CDX OC1=C(C2=C(C=C1)C[C@@]3([C@]45[H])[H])O[C@]([C@@]52CCN3C)([C@H](CC4)OC)[H],SMILES OC1=C(O2)C([C@]([C@]2(C)C(CC3)=O)(CCN4C)[C@]3([H])[C@H]4C5)=C5C=C1,SMILES ........ |
註意:你不需要將這些資料複製下來,本文結尾會給出原始碼,其中包含了這個完整的資料檔案。
實現過程
可以基於.NET Framework 4.6.1或者.NET Core建立一個新的控制檯應用程式,在這個控制檯應用程式上,新增對ML.NET NuGet包的取用。實現的第一步就是定義我們的樣本資料物件。根據上面的CSV檔案結構,我們可以設計如下的類:
public class ChemicalData { [Column( "0" )] public string ChemicalStructure; [Column( "1" )] public string Type; } |
這個類非常簡單,僅僅是針對CSV檔案兩個列的對映。接下來,我們需要定義用於儲存預測結果的資料物件,該物件不僅會用來儲存預測結果值,而且還會提供基於不同分類的可信度得分(Score):
public class ChemicalDataPrediction { [ColumnName( "PredictedLabel" )] public string Type; public float [] Score; } |
OK,到這裡我們基本上已經清楚我們的機器學習應用場景了:我們在使用Multi-class Classification對化學結構式資料進行分類。在機器學習的應用過程中,瞭解應用場景是非常重要的。然後,回到Main函式,實現如下程式碼:
static void Main( string [] args) { // 建立機器學習背景關係實體 var mlContext = new MLContext(); // 從data.txt讀入樣本資料 var dataView = mlContext.Data.ReadFromTextFile( "data.txt" , new TextLoader.Arguments { Separators = new char [] { ',' }, // 逗號分隔 HasHeader = false , // 檔案中不包含CSV頭資訊 Column = new [] { new TextLoader.Column( "ChemicalStructure" , DataKind.Text, 0), // 化學結構式資料欄位 new TextLoader.Column( "Type" , DataKind.Text, 1) // 化學結構式資料型別欄位 } }); // 建立機器學習管道,指定我們需要使用CSV檔案中的Type欄位進行標記並分類 var pipeline = mlContext.Transforms.Conversion.MapValueToKey( "Label" , "Type" ) // 指定將由ChemicalStructure欄位提供特徵資訊 .Append(mlContext.Transforms.Text.FeaturizeText( "Features" , "ChemicalStructure" )) // 選擇機器學習演演算法 .Append(mlContext.MulticlassClassification.Trainers.LogisticRegression()) // 計算結果將輸出到由PredictedLabel所標記的物件欄位上 .Append(mlContext.Transforms.Conversion.MapKeyToValue( "PredictedLabel" )); // 基於樣本資料和所選擇的管道選項,進行模型訓練,並傳回模型 var model = pipeline.Fit(dataView); // 建立預測引擎 var engine = model.CreatePredictionEngine(mlContext); // 對給定的測試資料進行預測,並輸出測試結果 var sample = new ChemicalData { ChemicalStructure = "NC(C(N)=O)=O" }; var prediction = engine.Predict(sample); Console.WriteLine(prediction.Type); } |
程式碼非常簡單,有幾個點說明一下:
- 新的ML.NET需要建立MLContext物件,所有的機器學習工作都會依賴於這個背景關係
- 透過MapValueToKey方法來指定讀入資料的哪個欄位是用來進行分類標記的,這個Label是ML.NET的一個保留欄位名,在模型訓練的時候會找到由Label所標記的欄位進行計算
- Features也是ML.NET的一個保留欄位名,它指定了哪個(或哪些)欄位將提供特徵資料
- PredictedLabel也是ML.NET的保留欄位名,它指定了計算結果應該輸出到哪個物件欄位中
直接執行程式,可以看到,程式毫無懸念地輸出了正確結果:
可信度得分的獲取
在上面的程式碼中,如果我們將斷點設定在最後一句Console.WriteLine方法上,然後除錯程式,檢視prediction的數值,會發現,各個分類的可信度已經在Score欄位裡了:
可問題是,我如何知道某個得分到底是屬於哪個分類呢?在ML.NET 0.6之前的版本,在訓練好的模型物件上,會有一個TryGetScoreLabelNames的擴充套件方法,它能夠傳回可信度得分的分類名稱,順序和Score陣列的順序一致。但從ML.NET 0.6開始,這個擴充套件方法已經沒有了,但這並不是說ML.NET變得更弱了,相反,新版本中直接將OutputSchema物件暴露出來,開發者可以自己實現所需的方法。下麵的程式碼展示瞭如何基於預測引擎的OutputSchema來獲取各個分類的名稱,以及所對應的可信度得分:
static void Main( string [] args) { // ... // 接上文程式碼 var outputSchema = engine.OutputSchema; TryGetScoreLabelNames(outputSchema, out var names); var confidences = new Dictionary< string , float >(); for ( var idx = 0; idx < names.Length; idx++) { confidences.Add(names[idx], prediction.Score[idx]); } Console.WriteLine(JsonConvert.SerializeObject( new { Label = prediction.Type, Confidences = confidences }, Formatting.Indented)); } static bool TryGetScoreLabelNames(Schema outputSchema, out string [] names, string scoreColumnName = DefaultColumnNames.Score) { names = ( string []) null ; var scoreColumn = outputSchema.GetColumnOrNull(scoreColumnName); var slotNames = new VBuffer scoreColumn.Value.GetSlotNames( ref slotNames); names = new string [slotNames.Length]; var num = 0; foreach ( var denseValue in slotNames.DenseValues()) { names[num++] = denseValue.ToString(); } return true ; } |
再次執行程式,可以看到,我們已經可以輸出各個分類的可信度得分了:
預測失誤
現在我們做個試驗,將最後用於測試的資料從SMILES換成INCHI,比如:
1
|
var sample = new ChemicalData { ChemicalStructure = "InChI=1S/ClH/h1H/p-1" }; |
然後再次執行程式,結果發現,我們本想得到INCHI的輸出,卻仍然得到SMILES的結果,只不過SMILES的可信度降低了,InChi的可信度升高了:
這個問題主要是因為我們所提供的用於訓練的樣本資料還不夠多,如果訓練資料量大,並且幹擾比較小的話,得到的預測結果就會更準確。因此,在實踐機器學習的過程中,保證訓練資料的純凈度和資料量是非常重要的,這也就是為什麼目前機器學習的專案中,在資料清洗這一步中有著相當大的投入。回到我們的案例,讓我們在樣本CSV檔案中多加一些InChi資料,來幫助機器學習得到更精確的結果:
"InChI=1/C2H6O/c1-2-3/h3H,2H2,1H3",InChi "InChI=1/C6H8O6/c7-1-2(8)5-3(9)4(10)6(11)12-5/h2,5,7-10H,1H2/t2-,5+/m0/s1",InChi "InChI=1S/C6H8O6/c7-1-2(8)5-3(9)4(10)6(11)12-5/h2,5,7-10H,1H2/t2-,5+/m0/s1",InChi "InChI=1S/CH4/h1H4",InChi "InChI=1S/C2H6/c1-2/h1-2H3",InChi "InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3",InChi "InChI=1S/C3H7NO2/c1-2(4)3(5)6/h2H,4H2,1H3,(H,5,6)/t2-/m0/s1",InChi "InChI=1S/ClH/h1H/p-1",InChi "InChI=1S/C6H7NO/c1-5-3-2-4-7-6(5)8/h2-4H,1H3,(H,7,8)",InChi "InChI=1S/CH2N2/c1-3-2/h1H2",InChi "InChI=1S/C7H5N3O/c11-7-5-3-1-2-4-6(5)8-10-9-7/h1-4H,(H,8,9,11)",InChi "InChI=1S/C8H6N2O/c11-8-6-3-1-2-4-7(6)9-5-10-8/h1-5H,(H,9,10,11)",InChi "InChI=1S/C2H6N2O/c1-4(2)3-5/h1-2H3",InChi "InChI=1S/C9H8N2O/c1-6-10-8-5-3-2-4-7(8)9(12)11-6/h2-5H,1H3,(H,10,11,12)",InChi "InChI=1S/C6H8O/c1-2-3-4-5-6-7/h2-6H,1H3/b3-2+,5-4+",InChi |
再次執行程式,我們已經可以得到正確的輸出了(雖然它仍然認為有31%的可能性是SMILES):
模型的儲存與使用
我們可以用下麵的程式碼將訓練好的模型儲存到本地ZIP檔案中,以便今後直接在專案中使用:
using ( var fs = new FileStream( "ml_model.zip" , FileMode.Create, FileAccess.Write, FileShare.Write)) { mlContext.Model.Save(model, fs); } |
然後使用下麵的程式碼,讀入儲存的模型,併進行新的預測:
var mlContext2 = new MLContext(); ITransformer loadedModel; using ( var stream = new FileStream( "ml_model.zip" , FileMode.Open, FileAccess.Read, FileShare.Read)) { loadedModel = mlContext2.Model.Load(stream); var engine2 = loadedModel.CreatePredictionEngine(mlContext2); var pred = engine2.Predict( new ChemicalData { ChemicalStructure = "c1ccccc1" }); Console.WriteLine(pred.Type); } |
總結
本文再一次介紹瞭如何使用微軟開源的ML.NET框架,實現化學結構式資料格式的預測和判定。本文對使用ML.NET的整個流程進行了詳細完整的介紹,但只演示了Multi-class Classification的應用場景。其它應用場景其實也大同小異,開發人員需要根據實際情況進行選擇。透過ML.NET產生的訓練模型是可以序列化到ZIP檔案的,因此,模型可以方便地重用。ML.NET支援.NET Core,因此,基於docker和ASP.NET Core實現機器學習的RESTful API也是輕而易舉的事情,本文就不繼續深入了。
原始碼下載
請 下載本文案例的原始碼http://sunnycoding.cn/archives/ML_ChemStructure_Demo.zip。
原文地址:http://sunnycoding.cn/2019/02/22/categorize-chemical-structure-using-ml-net-advanced/