資料相似度處理功能實現思路與方法
最近接到一個需求,需求直接來源於業務的一句話,“找出這堆商品資訊裡面相似的商品,根據名稱判斷”。
需求看似簡單,實則思考起來用技術實現是需要花點心思的。對於這樣的需求,首先要有一個思路和思考的過程:
1、業務具體想要的是什麼? -- 名稱相似度超過一定比例的兩個商品可以算成一個或者一組商品,用來後續組合處理
2、什麼樣的資料才算是相似的? -- 兩個名稱相似度超過70%,算是描述的同一種商品
3、怎麼實現相似度的計算的方法? -- 通過餘弦值演算法可以計算字串相似程度
4、什麼樣的資料結構提供給業務,是可以讓他們一眼看出來哪些資料是相似的? -- 通過SQL分組,並使用DENSE_RANK方法,將同組的資料進行數字編號。
5、演算法能否進行優化用來增加業務需求命中率和計算效能(除了餘弦值計算,分詞、單字切割、名詞加權計算、計算複雜度O(n)或O(n2)等等)?
對於當前需求,分為以上5步驟去進行考慮,區分哪些是權重價值比較高的行為,需要重點思考:
1、4代表著理解業務需求的程度,影響著這次結果能否讓需求提出方干係人的滿意
2、3、5代表著需求所需要思考的技術實現,影響著交付結果的質量
根據干係人需要的緊急程度和質量價值分析,我們可以將第5步驟作為可選項。
1、2、4與業務相關,此處不做詳細描述,針對於第3步驟,實現方法如下:
餘弦值演算法:
餘弦值越接近1,就表明夾角越接近0度,也就是兩個向量越相似,夾角等於0,即兩個向量相等,這就叫"餘弦相似性"。
舉例:
名稱A:紐西蘭紅玫瑰Queen蘋果12個140g以上/個
名稱B:紐西蘭紅玫瑰Queen蘋果6個150g以上/個
第一步,語句拆分
名稱A:新 西 蘭 紅 玫 瑰 Q u e e n 蘋 果 1 2 個 1 4 0 g 以 上 / 個
名稱B:新 西 蘭 紅 玫 瑰 Q u e e n 蘋 果 6 個 1 5 0 g 以 上 / 個
第二步,列出所有單字組合(去重)
總語句:新西蘭紅玫瑰Quen蘋果 12個40g以上/ 6 5
第三步,計算字頻
名稱A:新[1]西[1]蘭[1]紅[1]玫[1]瑰[1]Q[1]u[1]e[2]n[1]蘋[1]果[1]1[2]2[1]個[2]4[1]0[1]g[1]以[1]上[1]/[1]6[0]5[0]
名稱B:新[1]西[1]蘭[1]紅[1]玫[1]瑰[1]Q[1]u[1]e[2]n[1]蘋[1]果[1]1[1]2[0]個[2]4[0]0[1]g[1]以[1]上[1]/[1]6[1]5[1]
第四步,寫出字頻向量。
名稱A:(1,1,1,1,1,1,1,1,2,1,1,1,2,1,2,1,1,1,1,1,1,0,0)
名稱B:(1,1,1,1,1,1,1,1,2,1,1,1,1,0,2,0,1,1,1,1,1,1,1)
第五步,套用公式
值= (A1*B1+A2*B2+A3*B3+A1*B1+.......)/ 根號(A各項平方之和) * 根號(B各項平方之和)
值=
值= 26 / 根號(30) * 根號(27)
值= 26 / 28.4604
=0.9135
值越大越相似,=1則代表完全相同。=0則代表完全不同
程式碼如下:
/// <summary>
/// 計算字串名稱(餘弦值演算法)
/// </summary>
/// <param name="citem"></param>
/// <param name="pinfo"></param>
/// <returns></returns>
private bool ProcessIsMatch2(string 語句A , string 語句B)
{
var segmenter = new JiebaSegmenter();
double ThresholdValue = 0.7; //相似閾值
string sourceName = 語句A.Trim(' ').ToLower();
string targName = 語句B.Trim(' ').ToLower();
#region //第一、生成字包,第二步,列出所有的詞。
//單個位元組分詞
List<string> sourceNameL = sourceName.ToCharArray().Select(o => o.ToString()).ToList();
List<string> targNameL = targName.ToCharArray().Select(o => o.ToString()).ToList();
////結巴元件智慧分詞
//List<string> sourceNameL = segmenter.Cut(sourceName).Select(o => o.ToString()).ToList();
//List<string> targNameL = segmenter.Cut(targName).Select(o => o.ToString()).ToList();
List<string> allL = new List<string>();
sourceNameL.ForEach(o =>
{
if (!allL.Contains(o))
{
allL.Add(o);
}
});
targNameL.ForEach(o =>
{
if (!allL.Contains(o))
{
allL.Add(o);
}
});
#endregion
#region 名稱包含直接返回
//如果出現名稱包含則認為是相等
if (sourceName.IndexOf(targName) > 0)
{
return true;
}
if (targName.IndexOf(sourceName) > 0)
{
return true;
}
#endregion
#region 第三步,計算詞頻。
var sourceint = new List<int>();
var targint = new List<int>();
//遍歷原名稱集合,按照單字元判斷是否存在於目標字串中 ,例如: 可口可樂金裝300ml → 可 口 可 樂 金 裝 3 0 0 m l
allL.ForEach(o =>
{
sourceint.Add(sourceNameL.Count(sl => sl == o));
targint.Add(targNameL.Count(sl => sl == o));
});
#endregion
#region 第四部,餘弦值公式計算
//分子
int fenzi = 0;
for (int i = 0; i < allL.Count(); i++)
{
fenzi += sourceint[i] * targint[i];
}
//分母
int sourcemax = 0;
sourceint.ForEach(o =>
{
sourcemax += (o * o);
});
int tagmax = 0;
targint.ForEach(o =>
{
tagmax += (o * o);
});
var fenmu = Math.Sqrt(sourcemax) * Math.Sqrt(tagmax);
#endregion
#region 結果比對
double res = fenzi / fenmu;
#endregion
//判斷佔比
if (res > ThresholdValue)
{
return true;
}
return false;
}
此處設定的閾值為0.7,超過70%相似,則預設為相似資料。
針對於第5步優化建議:
1、分詞的工具,上述程式碼使用的是 單字分割,分詞工具有結巴分詞、盤古分詞、Lucene分詞等
2、考慮如果去掉無用的片語,例如:語氣詞、連線詞等
3、對名詞或關鍵片語設定加權規則,例如:美國(3)/紅蛇果(3)4個(1)180g(1)以上(1)/(0)個(1)
4、對於遍歷一組資料查詢一般情況下需要巢狀兩層遍歷,Foreach(){ Foreach(){ } },複雜度為O(n2),在計算時可以考慮第一層遍歷時,判斷為相似資料則進行打標關聯操作,後續遍歷則進行跳過打標資料,複雜度則小於O(n2),計算過程則約到後端速度越快。
針對於優化相似度演算法 或者 提升計算效能的方法還有很多,需求的實現任務範圍則需要根據業務的需要價值分析進行裁剪或擴充。