譯者 | 陳峻
審校 | 重樓
自去年以來,諸如ChatGPT 和 Bard之類的大語言模型已將機器學習提升到了一種現(xiàn)象級的地位。開發(fā)人員使用它們在輔助編程方面不斷探索了從圖像生成到疾病檢測等領域的應用案例。
鑒于全球各大科技公司都在加大針對機器學習的投入,作為Java開發(fā)人員有必要了解如何訓練和使用機器學習模型。下面,您將初步了解到機器學習的基本工作原理,有關如何實現(xiàn)和訓練機器學習算法的簡短指南,以及開發(fā)智能應用的最常用監(jiān)督機器學習方法。
機器學習和人工智能
總的說來,機器學習是從試圖模仿人類智慧的AI領域發(fā)展而來,使得應用程序能夠在無需人工參與的情況下,執(zhí)行流程改進,并按需更新代碼和擴展其功能。
目前,監(jiān)督學習和無監(jiān)督學習是兩種最流行的機器學習方法。這兩種方法都需要向機器輸入大量的數(shù)據記錄,以便其進行關聯(lián)和學習。而這些被收集到的數(shù)據記錄通常被稱為特征向量。例如,對于某個房屋類數(shù)據而言,特征向量可能包括了房屋的總體面積、房間數(shù)量、以及房齡等特征。
監(jiān)督學習
在監(jiān)督學習中,為了訓練算法,機器需要輸入一組特征向量和相關標簽。其中,標簽通常是由人類注釋者提供的,代表了對于某個給定問題的正確回答。學習算法會分析特征向量、及其正確標簽,以找出它們之間的內部結構和關系。據此,機器就能夠學會如何正確地回答問題。
舉例來說,一個智能房地產應用為了接受特征向量的訓練,人工標注者會根據房屋面積、房間數(shù)和房齡等因素,為每套房屋標注出正確的房價。通過對數(shù)據進行分析,該房地產應用將會被訓練成能夠回答“這套房子能賣多少錢?”的問題。
而且在完成訓練后,該應用即使碰到未見過的、未標記的特征向量,機器也能夠正確地回答新的查詢。
無監(jiān)督學習
在無監(jiān)督學習中,算法通過編程來預測答案,而無需人工標注,甚至無需提問。無監(jiān)督學習并非預先確定標簽或結果,而是利用海量數(shù)據集和處理能力,來發(fā)現(xiàn)以前未知的相關性。例如,在消費品的營銷過程中,無監(jiān)督學習可以被用于識別隱藏的關系或消費者分組,以最終形成新的或改進的營銷策略。
監(jiān)督機器學習項目
鑒于所有的機器學習都以數(shù)據為基礎,因此從本質上講,算法需要根據源于現(xiàn)實世界的各種數(shù)據實例的輸入,建立一套數(shù)學模型,以最終學會使用新的數(shù)據來預測未知的結果。
本文將重點介紹監(jiān)督學習,這一目前最常見的機器學習方法。讓我們延用上文提到的房地產應用案例,用一種有意義的方式為數(shù)據貼上標簽。在下表 1 中,房屋記錄的每一行都包含了一個“房價”標簽。通過將行數(shù)據與房價標簽相關聯(lián),算法最終將能夠預測不在其數(shù)據集中的房屋市場價(注意,房屋面積以平方米為單位,而房價以歐元為單位)。
表 1.房屋記錄
特征 | 特征 | 特征 | 標簽 |
房屋尺寸 | 房間數(shù)量 | 房屋年齡 | 估計費用 |
90平方米/295 英尺 | 2 | 23 年 | 249,000 歐元 |
101平方米/331 英尺 | 3 | 無 | 338,000 歐元 |
1330 平方米/4363 英尺 | 11 | 12 年 | 6,500,000 歐元 |
在早期階段,您可能需要手工標注數(shù)據記錄,但最終您將訓練應用自動完成該過程。也就是說,標記數(shù)據集僅用于訓練和測試目的。這一階段結束后,機器學習模型將能夠在無標簽數(shù)據的實例上工作。例如,您可以向預測算法輸入一條新的、無標簽的房屋記錄,它會根據曾經的訓練數(shù)據自動預測房價。
訓練機器學習模型
監(jiān)督機器學習的挑戰(zhàn)在于,為特定問題找到合適的預測函數(shù)。從數(shù)學角度講,我們的挑戰(zhàn)就是要找到接收輸入變量x,并能夠返回預測值的目標預測函數(shù)。
圖 1.目標預測函數(shù)示例
在大多數(shù)情況下,x代表了一個多數(shù)據點。而在該案例中,它是由房屋尺寸值和房間數(shù)量值定義的單個房屋的二維數(shù)據點。這些值的數(shù)組被稱為特征向量。為了預測單個房屋的價格,我們可以使用包含了房屋尺寸和房間數(shù)量的特征向量{ 101.0, 3.0 } 去調用目標預測函數(shù):
清單 1.使用特征向量調用目標預測函數(shù)
// 目標預測函數(shù) h(學習過程的輸出)
Function<Double[], Double> h = ...;
// 設置房屋尺寸=101 和房間數(shù)=3 的特征向量
Double[] x = new Double[] { 101.0, 3.0 };
// 并預測房價(標簽)
double y = h.apply(x);
在清單 1 中,數(shù)組變量x值代表房屋的特征向量。目標預測函數(shù)返回的y值是預測的房價。
機器學習面臨的挑戰(zhàn)是:定義一個盡可能準確地用于未知的、未見過的數(shù)據實例的目標預測函數(shù)。在機器學習中,目標預測函數(shù)(hθ)有時被稱為模型。模型是學習過程的結果,也被稱為模型訓練。
圖 2.機器學習模型
學習算法以標注的訓練示例為基礎,在訓練數(shù)據中尋找各種結構或模式。在此過程中,學習算法會逐步修改數(shù)值以減少損失,從而生成一個能夠從數(shù)據中進行泛化的模型。
由于機器學習的過程通常是探索性的,因此在大多數(shù)情況下,不同的學習算法和配置會被執(zhí)行多次。當一個模型被確定后,數(shù)據也會通過該模型被運行多次。這些迭代也被稱為epoch。
最后,算法將根據性能指標對所有模型進行評估,以選出最佳模型,用于計算對于那些未標記數(shù)據實例的預測。
線性回歸
首先,我們需要選擇待使用的學習算法。線性回歸是最簡單、也是最流行的監(jiān)督學習算法之一。該算法假定輸入特征和輸出標簽之間存在著線性關系。在圖 3 的公式中,通用線性回歸函數(shù)通過總結特征向量的每個元素,乘以一個theta 參數(shù) (θ) ,以返回預測值。在該訓練過程中,θ 參數(shù)被用于根據訓練數(shù)據,來調整回歸函數(shù)。
圖 3.通用線性回歸函數(shù)
線性回歸雖然是一種簡單的學習函數(shù),但是它為前饋式神經網絡(Forward Neural Network)中使用的梯度下降等更高級形式,奠定了良好的基礎。在線性回歸函數(shù)中,θ 參數(shù)和特征參數(shù)由訂閱數(shù)進行枚舉。此處的訂閱數(shù)表示theta 參數(shù) (θ) 和特征參數(shù) (x) 在向量中的位置。需要注意的是,特征x0是一個常數(shù)偏移項。為便于計算,其值被設為1。因此,針對特定領域特征(如房屋尺寸)的索引將從x1 開始。如果x1被設置為房屋特征向量的第一個值(房屋尺寸),那么x2將被設置為下一個值(房間數(shù)量),以此類推。
注意,在對線性回歸進行可視化時,您不妨想象一條在坐標系上的直線,它試圖盡可能地接近數(shù)據點。下面的清單 2 展示了該線性回歸函數(shù)的 Java 實現(xiàn)。它在數(shù)學上顯示為hθ(x)。為了簡單起見,該計算使用了double 數(shù)據類型。而在apply()方法中,數(shù)組的第一個元素已在該函數(shù)之外被設置為 1.0。
清單 2.Java 中的線性回歸
public class LinearRegressionFunction implements Function<Double[], Double> {
private final double[] thetaVector;
LinearRegressionFunction(double[] thetaVector) {
this.thetaVector = Arrays.copyOf(thetaVector, thetaVector.length);
}
public Double apply(Double[] featureVector) {
// 出于計算原因,第一個元素必須是 1.0
assert featureVector[0] == 1.0;
// simple, sequential implementation
double prediction = 0;
for (int j = 0; j < thetaVector.length; j++) {prediction += thetaVector[j] * featureVector[j];
}
return prediction;
}
public double[] getThetas() {
return Arrays.copyOf(thetaVector, thetaVector.length);
}
}
為了創(chuàng)建LinearRegressionFunction 的新實例,我們必須設置 theta 參數(shù)。theta 參數(shù)或向量被用于使得通用回歸函數(shù)能夠適應基礎的訓練數(shù)據。該代碼的 theta 參數(shù)將在學習過程中根據訓練示例進行調整。顯然,訓練目標預測函數(shù)的質量,只能與給定訓練數(shù)據的質量相當。
在下一個示例中,我們對LinearRegressionFunction進行實例化,以根據房屋尺寸預測房價。考慮到x0必須是 1.0 的常量,目標預測函數(shù)將使用兩個 theta 參數(shù)進行實例化。而theta 參數(shù)則是學習過程的輸出。在創(chuàng)建了新的實例后,面積為 1330 平方米的房屋價格預測結果如下:
// 這里使用的 Theta 向量是訓練過程的輸出
double[] thetaVector = new double[] { 1.004579, 5.286822 };
LinearRegressionFunction targetFunction = new LinearRegressionFunction(thetaVector);
// create the feature vector function with x0=1 (for computational reasons) and x1=house-size
Double[] featureVector = new Double[] { 1.0, 1330.0 };
// 進行預測
double predictedPrice = targetFunction.apply(featureVector);
目標預測函數(shù)的預測線在圖 4 中顯示為一條藍線。這條線是通過對所有房屋面積值進行目標預測計算得出的。同時,圖中還包括了用于訓練的“價格-尺寸”對。
圖 4 目標預測函數(shù)預測線
上圖坐標的截距和斜率是由θ 向量{ 1.004579, 5.286822 } 定義。該預測圖看似十分貼切,但是我們又怎么知道該 Theta 向量一定適合自己的應用呢?如果改變第一個或第二個 theta 參數(shù),函數(shù)的適合度會更好嗎?為了確定最合適的 theta 參數(shù)向量,我們需要一個實用函數(shù)(Utility Function)來評估目標預測函數(shù)的性能。
目標預測函數(shù)的評估
在機器學習中,成本函數(shù)(J(θ))(又稱“損失函數(shù)”)通常被用于計算給定目標預測函數(shù)的平均誤差或“成本”。圖 5 顯示了一個函數(shù)例子。
圖 5 成本函數(shù)
成本函數(shù)表示模型與訓練數(shù)據的適合程度。若要確定訓練目標預測函數(shù)的成本,我們可以計算每個房屋示例(i) 的平方誤差。此處的誤差是計算得出的y值,與房屋示例i 的實際y值之間的距離。例如,面積為 1330 平方米的房屋的實際價格為 6,500,000 歐元,而經過訓練的目標預測函數(shù)預測的房屋價格為 7,032,478 歐元,其差距(或誤差)為 532,478 歐元。您可以在圖 4中找到這一差距。圖中的差距(或誤差)是以垂直紅色虛線的形式,顯示在每個訓練“價格-尺寸”對中。
要計算經過訓練的目標預測函數(shù)的成本,我們必須總結示例中每棟房屋的平方誤差,并計算出平均值。只有J(θ) 的成本值越小,目標預測函數(shù)的預測才越精確。
在下面的代碼列表中,代價函數(shù)的簡單 Java 實現(xiàn)將目標預測函數(shù)、訓練記錄列表、及其相關標簽作為輸入。預測值將在循環(huán)中被計算,而誤差則通過減去真實標簽值來計算。據此,它將匯總平方誤差并計算平均誤差。成本也將以雙值的形式返回:
public static double cost(Function<Double[], Double> targetFunction,
List<Double[]> dataset,
List<Double> labels) {
int m = dataset.size();
double sumSquaredErrors = 0;
// 計算每個訓練示例的平方誤差(“差距”),并將其與總和相加
for (int i = 0; i < m; i++) {
// 獲取當前示例的特征向量
Double[] featureVector = dataset.get(i);
// 根據真實值(標簽)預測值并計算誤差
double predicted = targetFunction.apply(featureVector);
double label = labels.get(i); double gap = targetFunction.ap ply(f eat ureVector); double label = labels.get(i);doublegap= targetFunction.apply(featureVector).get(i);
double gap = predicted - label;sumSquaredErrors += Math.pow(gap, 2);
}
// 計算并返回誤差的平均值(越小越好)
return (1.0 / (2 * m)) * sumSquaredErrors;
}
用梯度下降法訓練目標預測函數(shù)
雖然代價函數(shù)有助于分別評估目標預測函數(shù)和 theta 參數(shù)的質量,但仍需要計算最佳適合的 theta 參數(shù)。在此,我們可以使用梯度下降算法來進行計算。
用梯度下降法計算 theta 參數(shù)
梯度下降法能夠使得成本函數(shù)最小化。也就是說,它能夠根據訓練數(shù)據找到產生最低成本(J(θ))的 Theta 組合。 梯度下降法通過使用部分導數(shù),逐步調整每個變量來實現(xiàn)這一點。 這是反向傳播的一種典型形式,而所有其他形式都會基于該形式。
圖 6 顯示了一種簡化算法,可用于計算新的、適合度更高的 Thetas:
圖 6.梯度下降使得成本函數(shù)最小化
在每次迭代中, Theta向量的每個參數(shù) θ 都會計算出一個新的、更好的值。同時,學習率α持續(xù)控制著每次迭代的計算步長。這種計算將會重復進行,直到得到一個適合且夠好的 θ 值組合。例如,圖 7 中的線性回歸函數(shù)就有三個 theta 參數(shù):
圖 7.帶有三個theta 參數(shù)的線性回歸函數(shù)
在每次迭代("epoch")中,算法都會并行地計算出每個 theta 參數(shù)的新值:θ0、θ1 和θ2。而每次迭代后,您都可以使用新的 Theta 向量{θ0,θ1,θ2} 再創(chuàng)建一個新的、適合度更高的LinearRegressionFunction實例。
清單 3 顯示了梯度下降算法的 Java 代碼。回歸函數(shù)的各個 theta 將使用訓練數(shù)據、數(shù)據標簽和學習率(α)來進行訓練。函數(shù)的輸出則使用由新的 theta 參數(shù)改進后的目標預測函數(shù)。其中,train()方法將會被反復調用,并輸入新的目標預測函數(shù)與上一次計算,得出的新 theta。這些調用都會重復進行,直到調整后的目標預測函數(shù)的成本達到最低點。
清單 3.Java 中的梯度下降算法示例
public static LinearRegressionFunction train(LinearRegressionFunction targetFunction,
List<Double[]> dataset,
List<Double> labels,
double alpha) {
int m = dataset.size();
double[] thetaVector = targetFunction.getThetas();
double[] newThetaVector = new double[thetaVector.length];
// 計算 theta 數(shù)組中每個元素的新 theta
for (int j = 0; j < thetaVector.length; j++) {
// 總結誤差差距 * 特征
double sumErrors = 0;
for (int i = 0; i < m; i++) {
Double[] featureVector = dataset.get(i);
double error = targetFunction.apply(featureVector) - labels.get(i);sumErrors += error * featureVector[j];
}
// 計算新的 theta 值
double gradient = (1.0 / m) * sumErrors;newThetaVector[j] = thetaVector[j] - alpha * gradient;
}
returnnew LinearRegressionFunction(newThetaVector);
}
若要驗證成本是否持續(xù)下降,我們可以在每個訓練步驟后執(zhí)行成本函數(shù)J(θ)。其預期結果是,每迭代一次,成本就必須降低一次。如果沒有減少的話,則說明學習率參數(shù)值過大,算法超過了最小值。那么在這種情況下,梯度下降算法就會失效。
為什么這種模式行不通
圖 8 顯示了使用計算出的新theta 參數(shù)的目標預測函數(shù)。其初始 Theta 向量為{ 1.0, 1.0 }。左側一列顯示的是迭代了 50 次后的預測圖;中間一列顯示的是迭代了 200 次后的預測圖;而右側一列顯示的是迭代了 1000 次后的預測圖。如圖所示,隨著新目標預測函數(shù)的適合效果越來越好,每次迭代后的成本都在降低。迭代 500 到 600 次后,θ 參數(shù)不再發(fā)生顯著變化,成本也就達到了穩(wěn)定的高點。此時,目標預測函數(shù)的精度將不再顯著提高。
圖 8.成本隨著每次迭代而降低
雖然經過 500 到 600 次迭代后,成本不再顯著降低,但是目標預測函數(shù)似乎仍然不是最優(yōu)的。在機器學習中,我們常用“欠適合(Underfitting)”一詞用來表示學習算法沒有捕捉到數(shù)據的潛在趨勢。
根據生活中的實際經驗,對于面積大的房產,其每平方米的預計價格會逐漸下降。據此,我們可以得出結論:該訓練過程中使用到的模型,即目標預測函數(shù),并沒能很好地適合真實數(shù)據。鑒于“欠適合”通常是由于模型過于簡單造成的,那么在本案例中,是由于我們的簡單目標預測函數(shù)僅使用了單一的房屋尺寸特征所造成。顯然,僅憑這些數(shù)據是不足以準確地預測房屋成本的。
添加和擴展特征
如果您發(fā)現(xiàn)目標預測函數(shù)不適合待解決的問題,一種常見的糾偏方法是,通過在特征向量中添加更多的特征來對其進行調整。例如,在前面的房屋價格示例中,您可以添加諸如:房間數(shù)量或房齡等更多房屋特征。同時,您也可以使用一個多值特征向量,如:{ 大小、房間數(shù)、房齡 },來描述房屋實例,而不僅僅使用{ 大小},這個單一的特定領域特征向量。
當然,在某些情況下,可用的訓練數(shù)據集中并沒有足夠的特征。那么您可以嘗試著添加由現(xiàn)有特征計算得出的多項式特征。例如,您可以擴展房價目標預測函數(shù),使其包含計算出的平方尺寸特征 (x2):
圖 9.利用多項式特征擴展的目標預測函數(shù)
值得注意的是,在使用多個特征時,我們需要進行特征的擴展(或“歸一化”),以規(guī)范不同特征的范圍。例如,size2特征的取值范圍就比 size 特征的取值范圍大一個量級。如果不對特征進行擴展,size2特征將主導整個成本函數(shù)。而size2特征所產生的誤差值將遠高于僅由尺寸特征產生的誤差值。下圖 10 顯示了一種簡單的特征擴展算法。
圖 10.簡單的特征擴展算法
該算法由如下Java 代碼清單中的FeaturesScaling類實現(xiàn)。FeaturesScaling類提供了一個工廠方法(Factory Method),可用于創(chuàng)建根據訓練數(shù)據調整的擴展函數(shù)。在內部,訓練數(shù)據的實例被用來計算平均值、最小值和最大值常數(shù)。而由此產生的函數(shù)會使用一個特征向量,并生成一個帶有擴展特征的新特征向量。如下圖所示,訓練過程和預測調用都需要對特征進行擴展:
// 創(chuàng)建數(shù)據集
List<Double[]> dataset = new ArrayList<>();dataset.add(new Double[] { 1.0, 90.0, 8100.0 }); // 房屋#1 的特征向量dataset.add(new Double[] { 1.0, 101.0, 10201.0 }); // 房屋#2 的特征向量dataset.add(new Double[] { 1.0, 103.0, 10609.0 }); // ...
//....
// 創(chuàng)建標簽
List<Double> labels = new ArrayList<>();labels.add(249.0); // 房屋#1的價格標簽labels.add(338.0); // 房屋#2的價格標簽labels.add(304.0); // ...
//...
// 擴展特征列表
Function<Double[], Double[]> scalingFunc = FeaturesScaling.createFunction(dataset);
List<Double[] > scaledDataset = dataset.stream().map(scalingFunc).collect(Collectors.toList());
// 使用初始值創(chuàng)建假設函數(shù),并以 0.1 的學習率對其進行訓練
LinearRegressionFunction targetFunction = new LinearRegressionFunction(new double[] { 1.0, 1.0, 1.0 });
for (int i = 0; i < 10000; i++) {targetFunction = Learner.train(targetFunction, scaledDataset, labels, 0.1);
}
// 對面積為 600 m2 的房屋進行預測
Double[] scaledFeatureVector= scalingFunc.apply(new Double[] { 1.0, 600.0, 360000.0 });
double predictedPrice = targetFunction.apply(scaledFeatureVector);
隨著更多的特征被添加,您可能會發(fā)現(xiàn)目標預測函數(shù)也適合得越來越好。不過值得注意的是,如果添加了太多的特征,則最終可能會導致目標預測函數(shù)出現(xiàn)過度適合(Overfitting)。
過度適合和交叉驗證
當目標預測函數(shù)或模型與訓練數(shù)據適合得好過頭時,就會出現(xiàn)過度適合。而過度適合的模型則會捕捉到訓練數(shù)據中的噪聲或隨機波動。圖 11 中最右側的圖表顯示了一種過度適合的行為模式。
圖 11.具有過度適合行為的目標預測函數(shù)示例
雖然過度適合模型在訓練數(shù)據上非常匹配,但當需要求解未知的、未見過的數(shù)據時,它的表現(xiàn)會相當糟糕。目前,我們有如下三種方法可以避免過度適合:
- 使用更大的訓練數(shù)據集。
- 通過增加正則化,來改進機器學習算法。
- 如上面的中間圖表所示,減少特征的使用。
如果您的預測模型出現(xiàn)過度適合的話,則應該刪除任何對其準確性無益的特征。此處的挑戰(zhàn)在于,如何找到對于預測結果貢獻最大的特征。
如上圖所示,過度適合可以通過可視化圖形來識別。盡管這種方法在使用二維或三維圖形時效果很好,但是如果使用兩個以上的特定域特征,則會變得相當困難。因此,交叉驗證經常被用來檢測過度適合。
在交叉驗證中,一旦學習過程結束,我們便可使用未見過的驗證數(shù)據集,對訓練好的模型進行評估。通常,我們可以將可用的標注數(shù)據集分為三個部分:
- 訓練數(shù)據集。
- 驗證數(shù)據集。
- 測試數(shù)據集。
在上述案例中,60% 的房屋示例記錄可用于訓練目標算法的不同變體。一旦學習過程結束,剩余的一半未接觸過的示例記錄,將被用于驗證訓練過的目標算法,是否能夠很好地處理未見過的數(shù)據。
通常情況下,我們會先選擇一種最合適的目標算法。而另一半未經處理的示例數(shù)據將用于計算最終選定模型的誤差指標。當然,這種技術也有其他的變種,比如:k 折交叉驗證等。此處就不展開了。
小結
在上文中,我們在了解機器學習相關概念的基礎上,介紹了一個監(jiān)督學習的示例,并使用梯度下降算法來訓練目標預測函數(shù)。同時,我們也討論了一種模型的欠適合示例,以及如何通過添加和擴展特征來實現(xiàn)糾偏。最后,我們還簡要介紹了過度適合的危險性,以及如何對其予以糾正。
譯者介紹
陳峻(Julian Chen),51CTO社區(qū)編輯,具有十多年的IT項目實施經驗,善于對內外部資源與風險實施管控,專注傳播網絡與信息安全知識與經驗。
原文標題:Machine learning for Java developers: Algorithms for machine learning,作者:Gregor Roth和Matthew Tyson