引言
技術(shù)無(wú)關(guān), 可跳過(guò).
最近在寫(xiě)一個(gè)獨(dú)立項(xiàng)目,
基于斗魚(yú)直播平臺(tái)的開(kāi)放接口, 對(duì)斗魚(yú)的彈幕進(jìn)行實(shí)時(shí)的分析,
最近抽空記錄一下其中一些我個(gè)人覺(jué)得值得分享的技術(shù).
在寫(xiě)這個(gè)項(xiàng)目的時(shí)候我一直在思考, 彈幕這種形式已經(jīng)出來(lái)了很久,
而且被廣大網(wǎng)友熱愛(ài), 確實(shí)增強(qiáng)了參與者之間的溝通,
但近年彈幕的形式卻沒(méi)什么很大的創(chuàng)新, 而問(wèn)題卻有許多,
其中有一條彈幕非常多的時(shí)候, 其實(shí)很多是重復(fù)的, 非常影響觀感.
于是我提出了一個(gè)需求: 實(shí)時(shí)采集彈幕, 并相互之間對(duì)比,
合并相近的彈幕, 這里的"相近"是個(gè)什么樣的標(biāo)準(zhǔn)就是值得去思考的一個(gè)東西了.
在查閱了很多資料之后, 發(fā)現(xiàn)這里已經(jīng)到了一個(gè)對(duì)自然語(yǔ)言處理的問(wèn)題,
說(shuō)大一點(diǎn)屬于AI的范疇了, 各大云平臺(tái)例如騰訊云都有這方面的功能,
蘋(píng)果最近WWDC發(fā)布的CoreML就可以使用訓(xùn)練好的自然語(yǔ)言識(shí)別模型.
在還不能用到CoreML(性能問(wèn)題有待斟酌)之前,
連接云平臺(tái)在瞬間高并發(fā)的使用場(chǎng)景下是不太現(xiàn)實(shí)的,
所以需要本地算出兩個(gè)中文句子的"語(yǔ)義近似度".
理論
編輯距離算法:
編輯距離,又稱(chēng)Levenshtein距離,是指兩個(gè)字串之間,
由一個(gè)轉(zhuǎn)成另一個(gè)所需的最少編輯操作次數(shù)。
許可的編輯操作包括將一個(gè)字符替換成另一個(gè)字符,插入一個(gè)字符,刪除一個(gè)字符。
每個(gè)操作成本不同, 最終可以得到一個(gè)編輯距離.
編輯距離越短, 句子就越相似, 編輯距離越長(zhǎng), 句子相似度就越低.
這種算法很早就被提出來(lái)了, 而且網(wǎng)上資料非常齊全, 先看算法:
#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
實(shí)際測(cè)試起來(lái), 這種算法由于對(duì)中文的適應(yīng)性不好, 會(huì)有各種問(wèn)題, 不細(xì)說(shuō)了.
繼續(xù)查資料, 看到另一種算法.
詞頻向量余弦?jiàn)A角算法:
這種算法思想也挺簡(jiǎn)單的,
將兩個(gè)句子構(gòu)造成兩個(gè)向量, 并計(jì)算這兩個(gè)向量的余弦?jiàn)A角cos(θ),
夾角為0°, 則代表兩個(gè)句子意思完全相同,
夾角為180°, 則代表兩個(gè)句子相似度為零.
下一個(gè)問(wèn)題, 怎樣將句子構(gòu)造成向量?
這里就引入"詞頻向量",
簡(jiǎn)單的說(shuō)就是先將兩個(gè)句子分詞,
通過(guò)詞第一次出現(xiàn)的位置以及詞出現(xiàn)的頻率組成向量,
再計(jì)算夾角.
舉個(gè)例子:
句子A: 斗魚(yú)伴侶真是有意思,支持斗魚(yú)直播
句子B: 斗魚(yú)伴侶挺有意思,斗魚(yú)直播可以用
分詞之后:
句子A: 斗魚(yú)/伴侶/真是/有意思/支持/斗魚(yú)/直播
句子B: 斗魚(yú)/伴侶/挺/有意思/斗魚(yú)/直播/可以/用
向量:
句子A:[2(斗魚(yú)),1(伴侶),1(真是),1(有意思),1(支持),1(直播)] (斗魚(yú)出現(xiàn)2次, 其他出現(xiàn)1次)
句子B:[2(斗魚(yú)),1(伴侶),1(挺),1(有意思),1(直播),1(可以),1(用)] (同上)
先看下面公式
分子就是2個(gè)向量的內(nèi)積
ab = 2x2(斗魚(yú)) + 1x1(伴侶) + 1x0(真是) + 1x0(挺) + 1x1(有意思) + 1x0(支持) + 1x1(直播) + 1x0(可以) + 1x0(用)
= 7
分母是兩個(gè)向量的模長(zhǎng)乘積
||a|| = sqrt(2x2(斗魚(yú)) + 1x1(伴侶) + 1x1(真是) + 1x1(有意思) + 1x1(支持) + 1x1(直播))
= 3
||b|| = 2x2(斗魚(yú)) + 1x1(伴侶) + 1x1(挺) + 1x1(有意思) + 1x1(直播) + 1x1(可以) + 1x1(用)
= 3.16....
最終可以得出來(lái)
cos θ = 0.737865
其實(shí)到此為止基本上可以判斷出這兩個(gè)句子的相似度了,
換算成角度其實(shí)更精確
similarity = arccos(0.737865) / M_PI
= 0.764166
參考文章: https://mp.weixin.qq.com/s/dohbdkQvHIGnAWR_uPZPuA
實(shí)際
下面具體說(shuō)說(shuō)這套算法思想的實(shí)現(xiàn)
這里面實(shí)際用起來(lái)有兩個(gè)難點(diǎn):
1.分詞: iOS系統(tǒng)其實(shí)自帶分詞Api, 只是對(duì)中文的支持并不是那么友好,
而且在高并發(fā)的情況下性能也堪憂, 自定義詞庫(kù)那是更加不能實(shí)現(xiàn)的了.
2.構(gòu)造向量并計(jì)算: 這個(gè)其實(shí)在iOS中直接構(gòu)造向量也是不那么好實(shí)現(xiàn)的,
因?yàn)樯婕暗絻蓚€(gè)句子詞的對(duì)比, 需要補(bǔ)0.
分詞
這里感謝開(kāi)源的分詞庫(kù) 結(jié)巴分詞
這個(gè)庫(kù)有各個(gè)語(yǔ)言的版本 其中iOS的版本地址:
https://github.com/yanyiwu/iosjieba
集成以及使用起來(lái)也非常簡(jiǎn)單, 性能也非常不錯(cuò)(蘋(píng)果自帶甩分詞不見(jiàn)了)
庫(kù)的底層是C++, 所以只是要注意的是用到庫(kù)的文件改為.mm后綴名.
結(jié)巴分詞支持自定義詞庫(kù) 直接將詞寫(xiě)入下面文件
注意不能空行 否則會(huì)報(bào)錯(cuò)
iosjieba.bundle/dict/user.dict.utf8
具體詞哪里來(lái)...
用抓包軟件在某些輸入法中抓的= =..
//初始化后直接使用
- (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);
}
//字符串轉(zhuǎn)詞數(shù)組
- (NSArray *)stringCutByJieba:(NSString *)string{
//結(jié)巴分詞, 轉(zhuǎn)為詞數(shù)組
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;
}
計(jì)算
上面已經(jīng)解決了分詞的問(wèn)題, 下面說(shuō)說(shuō)具體怎么算,
這里我沒(méi)有直接構(gòu)造向量解決, 并沒(méi)有太好的思路.
但是利用算法的思路和面向?qū)ο蟮乃枷胛沂沁@樣解決的:
我們需要得到的是向量的內(nèi)積和模長(zhǎng)乘積,
先說(shuō)模長(zhǎng)乘積, 這個(gè)數(shù)字是固定的, 跟對(duì)比的句子無(wú)關(guān), 比較好得到.
我們發(fā)現(xiàn)向量的內(nèi)積其實(shí)在這里跟詞的位置無(wú)關(guān),
所以可以用字典來(lái)構(gòu)造, key為詞, value為詞頻,
遍歷數(shù)組對(duì)比, 可以得到每個(gè)詞的詞頻, 構(gòu)造詞頻字典,
再將兩個(gè)字典相同key的value相乘即為模長(zhǎng)乘積.
說(shuō)起來(lái)有點(diǎn)繞, 看代碼:
//這里構(gòu)造了兩個(gè)BASentenceModel用來(lái)存原來(lái)的文本,分詞后的詞數(shù)組,以及詞頻字典.
在設(shè)置分詞數(shù)組時(shí)候遍歷數(shù)組得出詞頻
- (void)setWordsArray:(NSArray *)wordsArray{
_wordsArray = wordsArray;
//根據(jù)句子出現(xiàn)的頻率構(gòu)造一個(gè)字典
__block NSMutableDictionary *wordsDic = [NSMutableDictionary dictionary];
[wordsArray enumerateObjectsUsingBlock:^(NSString *obj1, NSUInteger idx1, BOOL * _Nonnull stop1) {
//若字典中已有這個(gè)詞的詞頻 +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;
}
//傳入兩個(gè)句子對(duì)象即可得出兩個(gè)句子之間的近似度
/**
余弦?jiàn)A角算法計(jì)算句子近似度
*/
- (CGFloat)similarityPercentWithSentenceA:(BASentenceModel *)sentenceA sentenceB:(BASentenceModel *)sentenceB{
//計(jì)算余弦角度
//兩個(gè)向量?jī)?nèi)積
//兩個(gè)向量模長(zhǎng)乘積
__block NSInteger A = 0; //兩個(gè)向量?jī)?nèi)積
__block NSInteger B = 0; //第一個(gè)句子的模長(zhǎng)乘積的平方
__block NSInteger C = 0; //第二個(gè)句子的模長(zhǎng)乘積的平方
[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;
}
結(jié)論
我知道很多人覺(jué)得這個(gè)挺沒(méi)有意義的,畢竟沒(méi)有人在前端上做這些事情..
但實(shí)際效果確實(shí)不錯(cuò), 在高峰彈幕期間彈幕合并大于1000+.
這里用的iphone6測(cè)試, 30秒1500條彈幕, 分詞就可以分成6000+,
再進(jìn)行各種分析(活躍度, 等級(jí), 詞頻, 句子, 禮物統(tǒng)計(jì), 篩選等等等),
這種強(qiáng)度下的計(jì)算, iphone完全無(wú)問(wèn)題, 多線程處理好之后如下圖:
相對(duì)于服務(wù)器高度依賴于數(shù)據(jù)庫(kù)計(jì)算, 受制于數(shù)據(jù)庫(kù)與硬盤(pán)性能來(lái)說(shuō),
內(nèi)存中的讀寫(xiě)顯然更有優(yōu)勢(shì), 問(wèn)題其實(shí)在ARC的情況下內(nèi)存的釋放不太受控制,
非常多彈幕的情況下可能會(huì)告警, 不過(guò)也只能這樣了.
畢竟海量彈幕模式PC打開(kāi)瀏覽器僅作展示都會(huì)卡死...
另一方面AI計(jì)算放在移動(dòng)設(shè)備上可能也是一種趨勢(shì),
蘋(píng)果推出CoreML希望在兼顧隱私的同時(shí),讓隨身設(shè)備更智能,
想象一下全球的手機(jī)都有AI系統(tǒng)獨(dú)立計(jì)算各種數(shù)據(jù), 數(shù)據(jù)存在云中再一次處理,
這會(huì)是一個(gè)很近而且很爆炸的未來(lái).
Github:https://github.com/syik/ZJSentenceAnalyze/tree/master
以上.
題外話:App已上架, 名字叫:直播伴侶, 功能點(diǎn)還挺多的
其中繪圖(quartz2D),動(dòng)畫(huà)(CoreAnimation/lottie)運(yùn)用的都挺多的.
感覺(jué)大家會(huì)有興趣, 有需要可以寫(xiě)寫(xiě)經(jīng)驗(yàn).
App大家可以下下來(lái)看看, 順便給個(gè)好評(píng), 3Q!