技術無關, 可跳過.前端
最近在寫一個獨立項目, 基於鬥魚直播平臺的開放接口, 對鬥魚的彈幕進行實時的分析, 最近抽空記錄一下其中一些我我的以爲值得分享的技術.ios
在寫這個項目的時候我一直在思考, 彈幕這種形式已經出來了好久, 並且被廣大網友熱愛, 確實加強了參與者之間的溝通, 但近年彈幕的形式卻沒什麼很大的創新, 而問題卻有許多, 其中有一條彈幕很是多的時候, 其實不少是重複的, 很是影響觀感.git
因而我提出了一個需求: 實時採集彈幕, 並相互之間對比, 合併相近的彈幕, 這裏的"相近"是個什麼樣的標準就是值得去思考的一個東西了.github
在查閱了不少資料以後, 發現這裏已經到了一個對天然語言處理的問題, 說大一點屬於AI的範疇了, 各大雲平臺例如騰訊雲都有這方面的功能, 蘋果最近WWDC發佈的CoreML就可使用訓練好的天然語言識別模型. 在還不能用到CoreML(性能問題有待斟酌)以前, 鏈接雲平臺在瞬間高併發的使用場景下是不太現實的, 因此須要本地算出兩個中文句子的"語義近似度".算法
編輯距離,又稱Levenshtein距離,是指兩個字串之間, 由一個轉成另外一個所需的最少編輯操做次數。 許可的編輯操做包括將一個字符替換成另外一個字符,插入一個字符,刪除一個字符。 每一個操做成本不一樣, 最終能夠獲得一個編輯距離. 編輯距離越短, 句子就越類似, 編輯距離越長, 句子類似度就越低.數據庫
這種算法很早就被提出來了, 並且網上資料很是齊全, 先看算法:數組
#import "NSString+Distance.h"
static inline int min(int a, int b) {
return a < b ? a : b;
}
@implementation NSString (Distance)
- (float)SimilarPercentWithStringA:(NSString *)stringA andStringB:(NSString *)stringB{
NSInteger n = stringA.length;
NSInteger m = stringB.length;
if (m == 0 || n == 0) return 0;
//Construct a matrix, need C99 support
NSInteger matrix[n + 1][m + 1];
memset(&matrix[0], 0, m + 1);
for(NSInteger i=1; i<=n; i++) {
memset(&matrix[i], 0, m + 1);
matrix[i][0] = i;
}
for(NSInteger i = 1; i <= m; i++) {
matrix[0][i] = i;
}
for(NSInteger i = 1; i <= n; i++) {
unichar si = [stringA characterAtIndex:i - 1];
for(NSInteger j = 1; j <= m; j++) {
unichar dj = [stringB characterAtIndex:j-1];
NSInteger cost;
if(si == dj){
cost = 0;
} else {
cost = 1;
}
const NSInteger above = matrix[i - 1][j] + 1;
const NSInteger left = matrix[i][j - 1] + 1;
const NSInteger diag = matrix[i - 1][j - 1] + cost;
matrix[i][j] = MIN(above, MIN(left, diag));
}
}
return 100.0 - 100.0 * matrix[n][m] / stringA.length;
}
@end
複製代碼
實際測試起來, 這種算法因爲對中文的適應性很差, 會有各類問題, 不細說了. 繼續查資料, 看到另外一種算法.瀏覽器
這種算法思想也挺簡單的, 將兩個句子構形成兩個向量, 並計算這兩個向量的餘弦夾角cos(θ), 夾角爲0°, 則表明兩個句子意思徹底相同, 夾角爲180°, 則表明兩個句子類似度爲零.bash
下一個問題, 怎樣將句子構形成向量? 這裏就引入"詞頻向量", 簡單的說就是先將兩個句子分詞, 經過詞第一次出現的位置以及詞出現的頻率組成向量, 再計算夾角.服務器
舉個例子: 句子A: 鬥魚伴侶真是有意思,支持鬥魚直播 句子B: 鬥魚伴侶挺有意思,鬥魚直播能夠用
分詞以後: 句子A: 鬥魚/伴侶/真是/有意思/支持/鬥魚/直播 句子B: 鬥魚/伴侶/挺/有意思/鬥魚/直播/能夠/用
向量: 句子A:[2(鬥魚),1(伴侶),1(真是),1(有意思),1(支持),1(直播)] (鬥魚出現2次, 其餘出現1次) 句子B:[2(鬥魚),1(伴侶),1(挺),1(有意思),1(直播),1(能夠),1(用)] (同上)
先看下面公式
分子就是2個向量的內積 ab = 2x2(鬥魚) + 1x1(伴侶) + 1x0(真是) + 1x0(挺) + 1x1(有意思) + 1x0(支持) + 1x1(直播) + 1x0(能夠) + 1x0(用) = 7
分母是兩個向量的模長乘積 ||a|| = sqrt(2x2(鬥魚) + 1x1(伴侶) + 1x1(真是) + 1x1(有意思) + 1x1(支持) + 1x1(直播)) = 3
||b|| = 2x2(鬥魚) + 1x1(伴侶) + 1x1(挺) + 1x1(有意思) + 1x1(直播) + 1x1(能夠) + 1x1(用) = 3.16....
最終能夠得出來 cos θ = 0.737865
其實到此爲止基本上能夠判斷出這兩個句子的類似度了, 換算成角度其實更精確 similarity = arccos(0.737865) / M_PI = 0.764166
參考文章: https://mp.weixin.qq.com/s/dohbdkQvHIGnAWR_uPZPuA
下面具體說說這套算法思想的實現 這裏面實際用起來有兩個難點: 1.分詞: iOS系統其實自帶分詞Api, 只是對中文的支持並非那麼友好, 並且在高併發的狀況下性能也堪憂, 自定義詞庫那是更加不能實現的了. 2.構造向量並計算: 這個其實在iOS中直接構造向量也是不那麼好實現的, 由於涉及到兩個句子詞的對比, 須要補0.
這裏感謝開源的分詞庫 結巴分詞 這個庫有各個語言的版本 其中iOS的版本地址: https://github.com/yanyiwu/iosjieba
集成以及使用起來也很是簡單, 性能也很是不錯(蘋果自帶甩分詞不見了) 庫的底層是C++, 因此只是要注意的是用到庫的文件改成.mm後綴名.
結巴分詞支持自定義詞庫 直接將詞寫入下面文件 注意不能空行 不然會報錯 iosjieba.bundle/dict/user.dict.utf8
具體詞哪裏來... 用抓包軟件在某些輸入法中抓的= =..
//初始化後直接使用
- (void)loadJieba{
NSString *dictPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"iosjieba.bundle/dict/jieba.dict.small.utf8"];
NSString *hmmPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"iosjieba.bundle/dict/hmm_model.utf8"];
NSString *userDictPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"iosjieba.bundle/dict/user.dict.utf8"];
const char *cDictPath = [dictPath UTF8String];
const char *cHmmPath = [hmmPath UTF8String];
const char *cUserDictPath = [userDictPath UTF8String];
JiebaInit(cDictPath, cHmmPath, cUserDictPath);
}
//字符串轉詞數組
- (NSArray *)stringCutByJieba:(NSString *)string{
//結巴分詞, 轉爲詞數組
const char* sentence = [string UTF8String];
std::vector<std::string> words;
JiebaCut(sentence, words);
std::string result;
result << words;
NSString *relustString = [NSString stringWithUTF8String:result.c_str()].copy;
relustString = [relustString stringByReplacingOccurrencesOfString:@"[" withString:@""];
relustString = [relustString stringByReplacingOccurrencesOfString:@"]" withString:@""];
relustString = [relustString stringByReplacingOccurrencesOfString:@" " withString:@""];
relustString = [relustString stringByReplacingOccurrencesOfString:@"\"" withString:@""];
NSArray *wordsArray = [relustString componentsSeparatedByString:@","];
return wordsArray;
}
複製代碼
上面已經解決了分詞的問題, 下面說說具體怎麼算, 這裏我沒有直接構造向量解決, 並無太好的思路. 可是利用算法的思路和麪向對象的思想我是這樣解決的:
咱們須要獲得的是向量的內積和模長乘積, 先說模長乘積, 這個數字是固定的, 跟對比的句子無關, 比較好獲得. 咱們發現向量的內積其實在這裏跟詞的位置無關, 因此能夠用字典來構造, key爲詞, value爲詞頻, 遍歷數組對比, 能夠獲得每一個詞的詞頻, 構造詞頻字典, 再將兩個字典相同key的value相乘即爲模長乘積.
提及來有點繞, 看代碼:
//這裏構造了兩個BASentenceModel用來存原來的文本,分詞後的詞數組,以及詞頻字典.
在設置分詞數組時候遍歷數組得出詞頻
- (void)setWordsArray:(NSArray *)wordsArray{
_wordsArray = wordsArray;
//根據句子出現的頻率構造一個字典
__block NSMutableDictionary *wordsDic = [NSMutableDictionary dictionary];
[wordsArray enumerateObjectsUsingBlock:^(NSString *obj1, NSUInteger idx1, BOOL * _Nonnull stop1) {
//若字典中已有這個詞的詞頻 +1
if (![[wordsDic objectForKey:obj1] integerValue]) {
__block NSInteger count = 1;
[wordsArray enumerateObjectsUsingBlock:^(NSString *obj2, NSUInteger idx2, BOOL * _Nonnull stop2) {
if ([obj1 isEqualToString:obj2] && idx1 != idx2) {
count += 1;
}
}];
[wordsDic setObject:@(count) forKey:obj1];
}
}];
_wordsDic = wordsDic;
}
//傳入兩個句子對象便可得出兩個句子之間的近似度
/**
餘弦夾角算法計算句子近似度
*/
- (CGFloat)similarityPercentWithSentenceA:(BASentenceModel *)sentenceA sentenceB:(BASentenceModel *)sentenceB{
//計算餘弦角度
//兩個向量內積
//兩個向量模長乘積
__block NSInteger A = 0; //兩個向量內積
__block NSInteger B = 0; //第一個句子的模長乘積的平方
__block NSInteger C = 0; //第二個句子的模長乘積的平方
[sentenceA.wordsDic enumerateKeysAndObjectsUsingBlock:^(NSString *key1, NSNumber *value1, BOOL * _Nonnull stop) {
NSNumber *value2 = [sentenceB.wordsDic objectForKey:key1];
if (value2.integerValue) {
A += (value1.integerValue * value2.integerValue);
}
B += value1.integerValue * value1.integerValue;
}];
[sentenceB.wordsDic enumerateKeysAndObjectsUsingBlock:^(NSString *key2, NSNumber *value2, BOOL * _Nonnull stop) {
C += value2.integerValue * value2.integerValue;
}];
CGFloat percent = 1 - acos(A / (sqrt(B) * sqrt(C))) / M_PI;
return percent;
}
複製代碼
我知道不少人以爲這個挺沒有意義的,畢竟沒有人在前端上作這些事情.. 但實際效果確實不錯, 在高峯彈幕期間彈幕合併大於1000+. 這裏用的iphone6測試, 30秒1500條彈幕, 分詞就能夠分紅6000+, 再進行各類分析(活躍度, 等級, 詞頻, 句子, 禮物統計, 篩選等等等), 這種強度下的計算, iphone徹底無問題, 多線程處理好以後以下圖:
相對於服務器高度依賴於數據庫計算, 受制於數據庫與硬盤性能來講, 內存中的讀寫顯然更有優點, 問題其實在ARC的狀況下內存的釋放不太受控制, 很是多彈幕的狀況下可能會告警, 不過也只能這樣了. 畢竟海量彈幕模式PC打開瀏覽器僅做展現都會卡死...
另外一方面AI計算放在移動設備上可能也是一種趨勢, 蘋果推出CoreML但願在兼顧隱私的同時,讓隨身設備更智能, 想象一下全球的手機都有AI系統獨立計算各類數據, 數據存在雲中再一次處理, 這會是一個很近並且很爆炸的將來.
Github:https://github.com/syik/ZJSentenceAnalyze/tree/master
以上. 題外話:App已上架, 名字叫:直播伴侶, 功能點還挺多的 其中繪圖(quartz2D),動畫(CoreAnimation/lottie)運用的都挺多的. 感受你們會有興趣, 有須要能夠寫寫經驗. App你們能夠下下來看看, 順便給個好評, 3Q!