《集體智慧編程》讀書筆記8
最近重讀《集體智慧編程》,這本當年出版的介紹推薦系統的書,在當時看來很引領潮流,放眼現在已經成了各互聯網公司必備的技術。
這次邊閱讀邊嘗試將書中的一些Python語言例子用C#來實現,利于自己理解,代碼貼在文中方便各位園友學習。
由于本文可能涉及到的與原書版權問題,請第三方不要以任何形式轉載,謝謝合作。
第八部分 核方法與SVM
這一部分繼續介紹一種分類器,以線性分類器為起點,逐漸引入和方法,最后到一種高階分類器-SVM。
本文中所使用的例子是婚介數據,示例數據分為兩個文件,簡單的數據只有年齡及是否匹配,而復雜的數據還包括是否吸煙,是否要孩子,興趣愛好等。
可以從這里下載這兩個文件,agesonly.csv與matchmaker.csv
這兩個文件的最后一列都表示是否匹配,0表示不匹配,1表示匹配。
和之前大部分文章所做的第一步一樣,我們首先要加載數據。
新建名為AdvancedClassify的類,在其中實現加載方法LoadMatch,同時還有一個表示一行數據的內部類MatchRow。
public List<MatchRow> LoadMatch(string file, bool allnum = false)
{
var rows = new List<MatchRow>();
var fs = File.OpenRead(file);
var sr = new StreamReader(fs);
while (!sr.EndOfStream)
{
var line = sr.ReadLine();
if (string.IsNullOrEmpty(line)) continue;
rows.Add(new MatchRow(line.Split(','), allnum));
}
return rows;
}
public class MatchRow
{
public readonly string[] Data;
public readonly double[] NumData;
public readonly int Match;
public MatchRow(string[] row, bool allnum = false)
{
if (allnum)
{
NumData = new double[row.Length - 1];
for (int i = 0; i < row.Length - 1; i++)
NumData[i] = double.Parse(row[i]);
}
else
{
Data = new string[row.Length - 1];
for (int i = 0; i < row.Length - 1; i++)
Data[i] = row[i];
}
Match = int.Parse(row.Last());
}
public MatchRow(double[] row)
{
NumData = row.Take(row.Length - 1).ToArray();
Match = (int)row.Last();
}
}
數據加載得到的結果就是一個MatchRow對象的列表。我們來測試下這個LoadMatch方法。從本文開始使用xUnit.net單元測試來測試方法,取代之前在控制臺程序Main函數里測試的方法。
在本系列文章結束后將把所有示例代碼上傳的Github,那時也會把之前幾篇文章中的測試方法以單元測試的方式來重構。
public class AdvancedClassifyTest
{
private readonly ITestOutputHelper _output;
public AdvancedClassifyTest(ITestOutputHelper output)
{
_output = output;
}
private void TestOutput(object obj)
{
_output.WriteLine(obj.ToString());
}
}
單元測試的方法都在上面這個類中,我們通過ITestOutputHelper來將結果輸出到測試工具的Output窗口(樓主使用Resharper來運行xUnit單元測試)。下面是測試數據加載的TestLoad方法:
[Fact]
public void TestLoad()
{
var advancedClassify = new AdvancedClassify();
var agesOnly = advancedClassify.LoadMatch(@"TestData\agesonly.csv", true);
_output.WriteLine(JsonConvert.SerializeObject(agesOnly));
var matchmaker = advancedClassify.LoadMatch(@"TestData\matchmaker.csv");
_output.WriteLine(JsonConvert.SerializeObject(matchmaker));
}
在上一篇文章中,我們介紹過使用MatplotlibCS生成坐標圖,這一部分我們利用之前的成果,把agesonly.csv所包含的數據進行可視化。我們把男方和女方的年齡分別作為X軸和Y軸的值。
生成函數圖像的方法為PlotageMatches,這里借用了上節實現的Draw方法,我們也將其原封不動的復制到當前類中:
public void PlotageMatches(List<MatchRow> rows)
{
var xdm = rows.Where(r => r.Match == 1).Select(r => r.NumData[0]).ToList();
var ydm = rows.Where(r => r.Match == 1).Select(r => r.NumData[1]).ToList();
var xdn = rows.Where(r => r.Match == 0).Select(r => r.NumData[0]).ToList();
var ydn = rows.Where(r => r.Match == 0).Select(r => r.NumData[1]).ToList();
var axes = new Axes(1, "", "")
{
Title = "Age Distribution",
ShowLegend = false
};
for (int i = 0; i < xdm.Count; i++)
axes.PlotItems.Add(new Point2D("go", xdm[i], ydm[i]) { Marker = Marker.Point });
for (int i = 0; i < xdn.Count; i++)
axes.PlotItems.Add(new Point2D("ro", xdn[i], ydn[i]) { Marker = Marker.Plus });
Draw(new List<Axes> { axes });
}
public void Draw(List<Axes> plots)
{
// 由于我們在外部啟動Python服務,這兩個參數傳空字符串就可以了
var matplotlibCs = new MatplotlibCS.MatplotlibCS("", "");
var figure = new Figure(1, 1)
{
FileName = $"/mnt/e/Temp/result{DateTime.Now:ddHHmmss}.png",
OnlySaveImage = true,
DPI = 150,
Subplots = plots
};
var t = matplotlibCs.BuildFigure(figure);
t.Wait();
}
對于不匹配的情況使用“圓點”來表示,而匹配的情況使用“加號”來表示。
接著寫在測試類里面寫一個測試方法來生成圖像
[Fact]
public void TestPlot()
{
var advancedClassify = new AdvancedClassify();
var agesOnly = advancedClassify.LoadMatch(@"TestData\agesonly.csv", true);
advancedClassify.PlotageMatches(agesOnly);
}
生成的分布圖如下:

建立這樣一個分布圖對于選擇分類方法是有幫助的。從圖中看,上篇文章介紹的決策樹對于這個問題的不是特別合適,因為產生的決策樹不易于解釋。而且當條件增加時,決策樹將變的更加復雜。
線性分類器
下面由最簡單的線性分類器開始,線性分類器的原理是尋找每個分類中所有數據的平均值,并構造一個代表該分類的中心位置點。通過比較帶分類數據與哪個中心位置點更近來進行分類。
首先實現計算分類的均值點的函數。本例中有兩個分類0和1,分表表示不匹配和匹配。
在AdvancedClassify中加入LinearTrain方法:
public Dictionary<int, double[]> LinearTrain(List<MatchRow> rows)
{
var averages = new Dictionary<int, double[]>();
var counts = new Dictionary<int, int>();
foreach (var row in rows)
{
//得到該坐標點所屬分類
var cl = row.Match;
if (!averages.ContainsKey(cl))
averages.Add(cl, new double[row.NumData.Length]);
if (!counts.ContainsKey(cl))
counts.Add(cl, 0);
//將該坐標點加入averages中
for (int i = 0; i < row.NumData.Length; i++)
{
averages[cl][i] += row.NumData[i];
}
//記錄每個分類中有多少坐標點
counts[cl] += 1;
}
// 將總和除以計數值以求得平均值
foreach (var kvp in averages)
{
var cl = kvp.Key;
var avg = kvp.Value;
for (var i = 0; i < avg.Length; i++)
{
avg[i] /= counts[cl];
}
}
return averages;
}
可以運行下面的測試得到線性分類的均值:
[Fact]
public void TestLinearTrain()
{
var advancedClassify = new AdvancedClassify();
var agesOnly = advancedClassify.LoadMatch(@"TestData\agesonly.csv", true);
var avgs = advancedClassify.LinearTrain(agesOnly);
_output.WriteLine(JsonConvert.SerializeObject(avgs));
}
輸出如下:
這表示分類1的中心點是(35.48,33.02),分類0的中心點是(26.91,35.89)。在進行分類時,只需要判斷待分類點更接近那個點即可。
這里將使用一種不同于之前介紹的歐幾里德距離的方法來判斷待分類點距離那個分類中心點更近,即向量點積。
向量點積的計算很簡單,即分別將第一個向量與第二個向量對應的值相乘,然后將乘機相加。表示為方法如下:
public double DotProduct(double[] v1, double[] v2)
{
return v1.Select((v, i) => v * v2[i]).Sum();
}
而向量點積的另一種計算方法就是使用向量長度的乘積再乘以兩向量夾角的余弦。結合兩種計算方法可以方便的求出兩個向量的余弦。
而向量點積有個重要特點,當夾角余弦為負值時,兩個向量夾角度數大于90度,反之兩個向量的夾角大于90度。
本例種我們不需要求出余弦具體值,只需要知道余弦的正負即可。由于向量長度的乘積一定為正,所以可知向量點積的符合與余弦的符號一定相同。所以我們只需要求出向量點積的正負即可。
我們按如下方法定義兩個向量,將相匹配分類的均值點M到不匹配分類的均值點N作為一個向量,待分類點X到M與N的中點作為另一個向量。當X距離M更近時,兩個向量的夾角將小于90度,反之大于90度。結合之前所說判斷向量夾角的方法可以很容易的計算出待分類點距離那個分類的中心點更近。
用公式來表示上面的過程(兩分類均值的中心點為\((M+N)/2\))
其中,sign表示符合函數,將公式展開后:
用將其轉為代碼實現為
public int DpClassify(double[] point, Dictionary<int, double[]> avgs)
{
var b = (DotProduct(avgs[1], avgs[1]) - DotProduct(avgs[0], avgs[0])) / 2;
var y = DotProduct(point, avgs[0]) - DotProduct(point, avgs[1]) + b;
if (y > 0) return 0;
return 1;
}
然后就可以嘗試進行線性分類了:
[Fact]
public void TestDpClassify()
{
var advancedClassify = new AdvancedClassify();
var agesOnly = advancedClassify.LoadMatch(@"TestData\agesonly.csv", true);
var avgs = advancedClassify.LinearTrain(agesOnly);
var classify = advancedClassify.DpClassify(new double[] { 30, 30 }, avgs);
_output.WriteLine(classify.ToString());
classify = advancedClassify.DpClassify(new double[] { 30, 25 }, avgs);
_output.WriteLine(classify.ToString());
classify = advancedClassify.DpClassify(new double[] { 25, 40 }, avgs);
_output.WriteLine(classify.ToString());
classify = advancedClassify.DpClassify(new double[] { 48, 20 }, avgs);
_output.WriteLine(classify.ToString());
}
線性分類器的一個明顯缺點就是待分類數據要有一條明顯的直線分界。如果找不到這樣一條直線,或有多條直線,則使用線性分類器很難有正確的結果。
分類特征
不同與決策樹,本文介紹的分類方法不能直接處理分類數據,我們需要先將其轉為數值類型。
對于“是否”問題的處理最簡單,將其轉為1或0,對于數據缺失的情況,可以直接按否處理:
public int YesNo(string v)
{
if (v == "yes") return 1;
if (v == "no") return -1;
return 0;
}
對于興趣列表,采用一種簡單粗暴的方法,即范圍兩人共同興趣的數量。雖然這會導致一定的偏差,比如滑板與滑雪有一定聯系,但采用這里的方法它們仍然會按0處理。復雜一點可以將興趣分為大類,并對這些大類進行比較給出一個0-1之間的關聯程度值。
public int MatchCount(string interest1, string interest2)
{
var l1 = interest1.Split(':');
var l2 = interest2.Split(':');
return l1.Intersect(l2).Count();
}
對于距離的確定,可以使用一些地圖網站提供的API將地址轉為經緯度并計算距離,但這里我們不做處理。返回0。
public int MilesDistance(string a1, string a2)
{
return 0;
}
構造新的數據集
有了上面的分類數據處理方法,我們可以將那份復雜的婚配數據處理為數值類型。
public List<MatchRow> LoadNumerical()
{
var oldrows = LoadMatch(@"TestData\matchmaker.csv");
var newrows = new List<MatchRow>();
foreach (var row in oldrows)
{
var d = row.Data;
var data = new[]
{
double.Parse(d[0]),
YesNo(d[1]),
YesNo(d[2]),
double.Parse(d[5]),
YesNo(d[6]),
YesNo(d[7]),
MatchCount(d[3], d[8]),
MilesDistance(d[4], d[9]),
row.Match
};
newrows.Add(new MatchRow(data));
}
return newrows;
}
驗證一下處理后的數值數據是否正確:
[Fact]
public void TestLoadNumerical()
{
var advancedClassify = new AdvancedClassify();
var numericalset = advancedClassify.LoadNumerical();
var dataRow = numericalset[0].Data;
_output.WriteLine(JsonConvert.SerializeObject(dataRow));
}
和之前很多篇文章相同,我們需要對數值數據進行縮放以使其據有可比性:
public Tuple<List<MatchRow>, Func<double[], double[]>> ScaleData(List<MatchRow> rows)
{
var low = ArrayList.Repeat(999999999.0, rows[0].NumData.Length).Cast<double>().ToList();
var high = ArrayList.Repeat(-999999999.0, rows[0].NumData.Length).Cast<double>().ToList();
// 尋找最大值和最小值
foreach (var row in rows)
{
var d = row.NumData;
for (int i = 0; i < row.NumData.Length; i++)
{
if (d[i] < low[i]) low[i] = d[i];
if (d[i] > high[i]) high[i] = d[i];
}
}
//對數據進行縮放處理的函數
// 注意:原書印刷代碼有問題,配書代碼是正確的,還要自己做一下“除0”的處理
Func<double[], double[]> scaleInput = d =>
low.Select((l, i) =>
{
if (high[i] == low[i]) return 0;
return (d[i] - low[i]) / (high[i] - low[i]);
}).ToArray();
//對所有數據進行縮放處理
var newrows = rows.Select(r =>
{
var newRow = scaleInput(r.NumData).ToList();
newRow.Add(r.Match);
return new MatchRow(newRow.ToArray());
}).ToList();
return Tuple.Create(newrows, scaleInput);
}
我們將數值數據縮放到0-1之間,并返回縮放函數。在后面分類時,我們需要首先對待分類數據進行縮放然后才能將其傳入分類器。
現在可以用更大規模的數據測試線性分類器了。
[Fact]
public void TestScaledLinearTrain()
{
var advancedClassify = new AdvancedClassify();
var numericalset = advancedClassify.LoadNumerical();
var result = advancedClassify.ScaleData(numericalset);
var scaledSet = result.Item1;
var scalef = result.Item2;
var avgs = advancedClassify.LinearTrain(scaledSet);
_output.WriteLine(JsonConvert.SerializeObject(numericalset[0].NumData));
_output.WriteLine(numericalset[0].Match.ToString());
_output.WriteLine(advancedClassify.DpClassify(scalef(numericalset[0].NumData), avgs).ToString());
_output.WriteLine(numericalset[11].Match.ToString());
_output.WriteLine(advancedClassify.DpClassify(scalef(numericalset[11].NumData), avgs).ToString());
}
通過結果可以看到線性分類器很難滿足多維數據處理的要求。下面將介紹一種新的分類方法。
核方法
如我們有這樣兩類數據,其中一個分類的數據分布于X(-2,2)Y(-2,2)之間的坐標軸中心位置,而另一個分類數據分布于這之外的坐標空間。一個分類的數據將另一個分類的數據環繞其中,所以這個明顯不可能使用線性分類來處理。但如果我們將這些數據進行變換,將每個點進行平方處理。則一個分類的數據將位于X(0,4)Y(0,4)這個坐標范圍,而另一個分類數據將落于X(4,+∞)Y(4,+∞)這個范圍。很明顯,這樣處理后可以使用一條直線將兩種分類劃分開來。
而進行分類時只需要將待分類數據進行平方并傳入分類器進行判斷即可。
當然實際例子中可能要進行多次變換才能將數據轉為線性分類可處理的數據。
實際編程實現中不會使用上面介紹的那種映射方法,使用這種特定的變化方法很難將多維的數據轉為高維空間中有明確分界線的數據。一種更好的方法被稱為核技法,其適用于任何使用點積運算算法的問題。
核技法的目的就是將一個線性分類器轉換成非線性分類器。
核技法可以采用多種不同的映射方法,一種備受推崇,也是本文要介紹的方法稱為徑向基函數(radial-basis function簡稱RBF)。
核方法將使用映射方法代替點積函數,新函數返回高維度坐標空間中的點積結果。(個人感覺原書要表達的意思是,映射方法可以像點積函數那樣利用正負對傳入值進行分類而不是真正返回點積結果)所以我們可以將這個映射函數套用于之前得到的公式。
將其中的點積函數換成我們的映射方法,即徑向基函數。
另一個問題,這個公式需要分類數據的均值點,而目前均值點是在原始坐標空間計算得到,這個均值點無法直接使用,而又無法計算新坐標空間中的均值點,因為不會計算每一個點在新坐標空間的位置。但,先對一組向量求均值,然后再計算均值與向量A的點積與先計算一組向量中每一個向量與向量A的點積然后計算均值是等價的。
有了上面的這些積累,下面就可以開始代碼實現了
首先實現徑向基函數
public double Rbf(double[] v1, double[] v2, int gamma = 20)
{
var dv = v1.Select((v, i) => v - v2[i]).ToArray();
var l = VecLength(dv);
return Math.Pow(Math.E, -gamma * l);
}
public double VecLength(double[] v)
{
return v.Sum(p => p * p);
}
徑向基函數的簽名與向量點積計算方法類似。但其是非線性的,可以將數據映射到更為復雜的空間,而且通過調整參數gamma還可以針對特定數據集得到最佳線性分離。
然后就是利用徑向基函數進行分類的方法:
public int NlClassify(double[] point, List<MatchRow> rows, double offset, int gamma = 10)
{
double sum0 = 0;
double sum1 = 0;
var count0 = 0;
var count1 = 0;
foreach (var row in rows)
{
if (row.Match == 0)
{
sum0 += Rbf(point, row.NumData, gamma);
++count0;
}
else
{
sum1 += Rbf(point, row.NumData, gamma);
++count1;
}
}
var y = sum0 / count0 - sum1 / count1 + offset;
if (y > 0) return 0;
return 1;
}
public double GetOffset(List<MatchRow> rows, int gamma = 10)
{
var l0 = new List<double[]>();
var l1 = new List<double[]>();
foreach (var row in rows)
{
if (row.Match == 0)
l0.Add(row.NumData);
else
l1.Add(row.NumData);
}
var sum0 = (from v2 in l0 from v1 in l0 select Rbf(v1, v2, gamma)).Sum();
var sum1 = (from v2 in l1 from v1 in l1 select Rbf(v1, v2, gamma)).Sum();
return sum1 / (l1.Count * l1.Count) - sum0 / (l0.Count * l0.Count);
}
其中,GetOffset就是計算公式中\(M\cdot M-N\cdot N\)這一部分。由于每次分類過程,這個值都是固定的,我們可以事先將其計算存儲。
下面嘗試下使用核方法預測的效果,首先使用只有年齡的數據:
[Fact]
public void TestNlClassify()
{
var advancedClassify = new AdvancedClassify();
var agesOnly = advancedClassify.LoadMatch(@"TestData\agesonly.csv", true);
var offset = advancedClassify.GetOffset(agesOnly);
TestOutput(advancedClassify.NlClassify(new[] { 30.0, 30 }, agesOnly, offset));
TestOutput(advancedClassify.NlClassify(new[] { 30.0, 25 }, agesOnly, offset));
TestOutput(advancedClassify.NlClassify(new[] { 25.0, 40 }, agesOnly, offset));
TestOutput(advancedClassify.NlClassify(new[] { 48.0, 20 }, agesOnly, offset));
}
從結果可以看出,核方法比起普通線性分類好了不少。接著嘗試下復雜數據的處理:
[Fact]
public void TestNlClassifyMore()
{
var advancedClassify = new AdvancedClassify();
var numericalset = advancedClassify.LoadNumerical();
var result = advancedClassify.ScaleData(numericalset);
var scaledSet = result.Item1;
var scalef = result.Item2;
var ssoffset = advancedClassify.GetOffset(scaledSet);
TestOutput(numericalset[0].Match);
TestOutput(advancedClassify.NlClassify(scalef(numericalset[0].NumData), scaledSet, ssoffset));
TestOutput(numericalset[1].Match);
TestOutput(advancedClassify.NlClassify(scalef(numericalset[1].NumData), scaledSet, ssoffset));
TestOutput(numericalset[2].Match);
TestOutput(advancedClassify.NlClassify(scalef(numericalset[2].NumData), scaledSet, ssoffset));
var newrow = new[] { 28, -1, -1, 26, -1, 1, 2, 0.8 };//男士不想要小孩,而女士想要
TestOutput(advancedClassify.NlClassify(scalef(newrow), scaledSet, ssoffset));
newrow = new[] { 28, -1, 1, 26, -1, 1, 2, 0.8 };//雙方都想要小孩
TestOutput(advancedClassify.NlClassify(scalef(newrow), scaledSet, ssoffset));
}
支持向量機
支持向量機的主要思想就是找到一條盡可能遠離所有分類的線,這條線被稱為最大間隔超平面(maximum-margin hyperplane)。
選擇這個分界線的方法是,尋找兩條分別經過各分類相應坐標點的平行線,并使其與分界線的距離盡可能的遠。只有位于間隔區域邊緣的坐標點才是確定分界線所必須的,抹去其余所有數據也不影響分界線的位置。這條分界線附近的點就稱為支持向量。尋找支持向量,并利用支持向量來尋找分界線的算法便是支持向量機(SVM)。
這樣對于待分類的點,只要判斷其位于分類線的哪一側就可以知道所屬的分類。
支持向量機所使用的也是點積的結果,因此也可以利用核技法將其用于非線性分類。
從零開始實現一個支持向量機非常復雜,有一個很好的開源支持向量機可以用于我們的例子 - LibSVM。LibSVM庫可以對SVM模型進行訓練,給出預測及利用數據集對預測結果進行測試,同時其也提供了類似徑向基函數這樣的核方法的支持。
這里使用LibSVM庫的一個C#包裝 - LibSVMsharp來進行分類測試。
首先給出一個簡單的例子,其可以讓你快速了解LibSVMsharp的使用:
[Fact]
public void LibsvmFirstLook()
{
var prob = new SVMProblem();
prob.Add(new[] { new SVMNode(1, 1), new SVMNode(2, 0), new SVMNode(3, 1) }, 1);
prob.Add(new[] { new SVMNode(1, -1), new SVMNode(2, 0), new SVMNode(3, -1) }, -1);
var param = new SVMParameter();
param.Kernel = SVMKernelType.LINEAR;
param.C = 10;
var m = prob.Train(param);
TestOutput(m.Predict(new []{new SVMNode(1,1), new SVMNode(2, 1), new SVMNode(3, 1) }));
m.SaveModel("trainModel");
var ml = SVM.LoadModel("trainModel");
TestOutput(ml.Predict(new[] { new SVMNode(1, 1), new SVMNode(2, 1), new SVMNode(3, 1) }));
}
所有LibSVM的使用代碼都直接實現在測試方法中
運行測試方法,需要手動拷貝libsvm.dll到Debug目錄下
代碼中首先構造訓練數據,指定分類方法,對模型進行訓練,然后進行預測。訓練模型也可以進行保存,并在未來加載用于分類。
有了LibSVMsharp的使用介紹,可以對婚配數據進行預測。
[Fact]
public void TestLibsvmClassify()
{
var advancedClassify = new AdvancedClassify();
var numericalset = advancedClassify.LoadNumerical();
var result = advancedClassify.ScaleData(numericalset);
var scaledSet = result.Item1;
var scalef = result.Item2;
var prob = new SVMProblem();
foreach (var matchRow in scaledSet)
{
prob.Add(matchRow.NumData.Select((v,i)=>new SVMNode(i+1,v)).ToArray(),matchRow.Match);
}
var param = new SVMParameter() {Kernel = SVMKernelType.RBF};
var m = prob.Train(param);
m.SaveModel("trainModel");
Func<double[], SVMNode[]> makeInput = ma => scalef(ma).Select((v, i) => new SVMNode(i + 1, v)).ToArray();
var newrow = new[] { 28, -1, -1, 26, -1, 1, 2, 0.8 };//男士不想要小孩,而女士想要
TestOutput(m.Predict(makeInput(newrow)));
newrow = new[] { 28, -1, 1, 26, -1, 1, 2, 0.8 };//雙方都想要小孩
TestOutput(m.Predict(makeInput(newrow)));
}
代碼依然很容易理解,先將婚配數據轉為LibSVM所需要的輸入,執行分類方法為徑向基函數,進行訓練然后測試分類。
為了達到更好的預測結果,可以使用前文的CrossValidate方法進行交叉驗證,然后調整LibSVM參數再次進行交叉驗證,直到得到滿意的結果。
優缺點
支持向量機是非常強大的分類方法,只要提供合適的參數,支持向量機的分類效果至少達到甚至超過之前介紹的其他分類器。另外分類速度快也是支持向量機的一個優勢,因為其只需要判斷坐標位于分界線的哪一側。
支持向量機的缺點也在于參數,對于每種不同的訓練數據,我們都需要確定參數。這就需要反復用交叉驗證去確定合適的參數。所以支持向量機更適于有大量訓練數據的場景。另外支持向量機和神經網絡都是黑盒技術 - 我們很難去解釋其給出結果的具體細節。

浙公網安備 33010602011771號