if(sbtmp.charAt(i)==','&&(sbtmp.charAt(i-1)==','||sbtmp.charAt(i+1)==',')){
sbtmp.deleteCharAt(i);
--i;
}
//去中间单个字
elseif(sbtmp.charAt(i-1)==','&&sbtmp.charAt(i+1)==','){
sbtmp.deleteCharAt(i);
sbtmp.deleteCharAt(i);
--i;
}
//去首个单个字
elseif(sbtmp.charAt(i)==','&&i==1){
sbtmp.deleteCharAt(i-1);
sbtmp.deleteCharAt(i-1);
--i;
}
}
3.1.2提取关键词
分词并不能很好的将常用的短语提取出来,如词语“用户画像”,使用分词工具更倾向于将其分成“用户”和“画像”,而失去了词语本身的含义。
NLPIR还提供了提取一段话的关键词的功能,我们可以使用它:
intnumofIm=1000;
StringnativeByte=CLibrary.Instance.NLPIR_GetKeyWords(sInput,numofIm,false);
经过分词后,平均每位用户搜索词列所得到的词量在600个左右,这里我们设置提取1000个关键词,但实际上一个用户的关键词提取的数量在60~200左右。
由于关键词的很强的特征性,并且提取出的数量又少,若后续我们直接使用如词语的词频作为用户的特征属性进行分类的话,很可能各个用户特征属性有巨大的差异,即用户之间拥有的相同关键词过少。
3.1.3混合提取
在用户搜索词列分词基础上,在增加N次对其进行M个关键词提取的结果。
3.2“结巴”分词
jieba,即“结巴”中文分词,一个优秀的开源的分词工具,一直致力于做最好的Python中文分词组件。
我们直接使用它对用户搜索词列进行1000个关键词的提取,所能提取到的关键词比NLPIR数量有所提高。
显然,关键词提取的数量增加,每个关键词的代表性就有所减弱。
但在后续的分类实验中证明了,使用该分词方案,对比上节的各个分词方案,在模型相同的情况下,会有2%~5%的准确率的提升。
关键词抽取可基于以下两种算法,后续实验实践证明基于TF-IDF算法的关键词的抽取,在该数据集和我们后续所选择的模型中会得到更好的效果。
3.2.1基于TF-IDF算法的关键词抽取
importjieba.analyse
jieba.analyse.extract_tags(sentence,topK=20,withWeight=False,allowPOS=())
sentence为待提取的文本
topK为返回几个TF/IDF权重最大的关键词,默认值为20
withWeight为是否一并返回关键词权重值,默认值为False
allowPOS仅包括指定词性的词,默认值为空,即不筛选
jieba.analyse.TFIDF(idf_path=None)新建TFIDF实例,idf_path为IDF频率文件
代码示例(关键词提取)
importsys
sys.path.append('../')
importjieba
importjieba.analyse
fromoptparseimportOptionParser
USAGE="usage:
pythonextract_tags.py[filename]-k[topk]"
parser=OptionParser(USAGE)
parser.add_option("-k",dest="topK")
opt,args=parser.parse_args()
iflen(args)<1:
print(USAGE)
sys.exit
(1)
file_name=args[0]
ifopt.topKisNone:
topK=10
else:
topK=int(opt.topK)
content=open(file_name,'rb').read()
tags=jieba.analyse.extract_tags(content,topK=topK)
print(",".join(tags))
3.2.2基于TextRank算法的关键词抽取
jieba.analyse.textrank(sentence,topK=20,withWeight=False,allowPOS=(‘ns’,‘n’,‘vn’,‘v’))直接使用,接口相同,注意默认过滤词性。
jieba.analyse.TextRank()新建自定义TextRank实例
基本思想[1]:
将待抽取关键词的文本进行分词
以固定窗口大小(默认为5,通过span属性调整),词之间的共现关系,构建图
计算图中节点的PageRank,注意是无向带权图
4.特征抽取与转换
4.1TF-IDF
TF(TermFrequency)词频
DF(DocumentFrequency)词语出现的文档数目
N总共的文档数目
IDF(InvertDocumentFrequency)逆文档频率
这里写图片描述
IDF(InvertDocumentFrequency)逆文档频率
这里写图片描述
IDF反映了一个特征词在整个文档集合中的情况,出现的愈多IDF值越低,这个词区分不同文档的能力越差。
示例代码:
importorg.apache.spark.ml.feature.{HashingTF,IDF,Tokenizer}
valsentenceData=spark.createDataFrame(Seq(
(0,"HiIheardaboutSpark"),
(0,"IwishJavacouldusecaseclasses"),
(1,"Logisticregressionmodelsareneat")
)).toDF("label","sentence")
valtokenizer=newTokenizer().setInputCol("sentence").setOutputCol("words")
valwordsData=tokenizer.transform(sentenceData)
valhashingTF=newHashingTF().setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(20)
valfeaturizedData=hashingTF.transform(wordsData)
validf=newIDF().setInputCol("rawFeatures").setOutputCol("features")
validfModel=idf.fit(izedData)
valrescaledData=idfModel.transform(featurizedData)
rescaledData.select("features","label").take(3).foreach(println)
/*输出结果为:
[(20,[0,5,9,17],[0.6931471805599453,0.6931471805599453,0.28768207245178085,1.3862943611198906]),0]
[(20,[2,7,9,13,15],[0.6931471805599453,0.6931471805599453,0.8630462173553426,0.28768207245178085,0.28768207245178085]),0]
[(20,[4,6,13,15,18],[0.6931471805599453,0.6931471805599453,0.28768207245178085,0.28768207245178085,0.6931471805599453]),1]
*/
值得一提的是,Spark所提供的TF并不是数组,而是一个使用MurmurHash3函数的哈希表。
其默认向量维度为2^18=262,144。
我们运行上节示例代码可以发现,我们将哈希表大小设置成了20,第二条sentence:
”IwishJavacouldusecaseclasses”有7个不同的单词,经过hash函数却被映射成了只有5个属性为非零值,即有2个位置放了2个不同的单词。
这具有很大的随机性,将两个无关词义的词语,甚至词义相反的词语,如“男”与“女”,映射到哈希表的同一位置,作为相同的用户属性来处理。
4.2CountVectorizer
为了解决上节所提到的HashingTF哈希函数映射后导致词语重叠问题,我们使用了Spark的CountVectorizer。
我们会先想CountVectorizer传入一个互斥的字符串数组,文本经过CountVectorizer转换后,会对该数组中所有的词语进行与属性的一一对应。
我们对互斥的字符串数组进行的优化,过滤掉了词频为1的词语,将CountVectorizer的维度减少到原来的50%,极大的降低了后续训练模型时所需的内存,而且除去的数据噪音,增加了预测的准确度:
valdiffTrain=Triandata.map{line=>
valtemp=line.split("\t")
if(temp.length==5)temp(4)else""
}
valdiffTest=Testdata.map{line=>
valtemp=line.split("\t")
if(temp.length==5)temp
(1)else""
}
valdiffAll=diffTrain.union(diffTest).flatMap(_.split(",")).map((_,1)).reduceByKey(_+_).collect.filter(line=>line._1!
=""&&line._2>14).map(line=>line._1)
valcvm=newCountVectorizerModel(diffAll).setInputCol(tokenizer.getOutCol).setOutputCol("features")
4.3StopWordsRemover
在上一章中,我们提到了分词时,根据分词结果所带的词性,对其进行去停用词。
而后,我们发现使用”结巴”分词进行TF-IDF算法对用户搜索词列进行1000个关键词的提取对于后续的分类模型效果会更好。
但是,我们在“结巴”关键词提取的结果却发现了类似于“什么”“即使”等不具有代表性的词语。
于是我们1119个停用词,使用Spark的StopWordsRemover,对分词结果进行去停用词:
valStopdata=sc.textFile("hdfs:
//cdh01:
8020//user/data/sogou2/stop",128).collect()
valremover=newStopWordsRemover().setInputCol("words").setOutputCol("filtered").setStopWords(Stopdata)
4.4权值规范化
设想两个不同的用户A和用户B,用户A的搜索词列中只有1句查询语句,分词后得到了3个词语W和总共10个词。
而用户B的搜索词列中有10句查询语句,分词后得到了10个词语W和总共100个词。
很显然,B中W的TF远高于A中的W的TF,但我们知道词语W在A中比在B中更具有代表性。
为了解决上述问题,我们使用了最大-最小规范化:
将所有特征向量线性变换到用户指定最大-最小值之间。
但注意在计算时还是一个一个特征向量分开计算的。
通常将最大,最小值设置为1和0,这样就归一化到[0,1]。
Spark中可以对min和max进行设置,默认就是[0,1]。
这里写图片描述
在后续,当我们对特征矩阵进行聚类后,得到的特征值可能为负值,可是很多分类器模型需要特征值为非负值。
使用以上方法也可以解决这个问题。
4.5同义词替换
设想当一个用户的搜索词列的分词结果中出现了一些意思相近的词语,如“恋爱”与“爱情”、“菠萝”与“凤梨”。
而我们的模型将其辨别为不同的特征属性,这无疑大量的增加了特征向量的维度和平分了同一意思的词语具有的代表性。
为了解决上述问题,我们搜集了近4万条同义词词典,将意思相近的词语由1个词语来替换掉。
该优化帮助原本的特征向量减少了3万以上的维度,降低了后续训练模型时所需的内存,而且凝聚了属性的代表性,增加了预测的准确度:
valsqlContext=neworg.apache.spark.sql.SQLContext(sc)
importsqlContext.implicits._
valtrain=sc.textFile("hdfs:
//cdh01:
8020//user/data/sogou2/JBtrain",400)
valtest=sc.textFile("hdfs:
//cdh01:
8020//user/data/sogou2/JBtest",400)
valsame=sc.textFile("hdfs:
//cdh01:
8020//user/data/sogou2/same",400)
same.filter{x=>!
x.contains('=')}.count()
valsameWord=same.map{line=>
valvaluekey=line.split('=')
(valuekey
(1),valuekey(0))
}.collect()
valbroadcastVar=sc.broadcast(sameWord)
valdiffTrain=train.map{line=>
valbroad=bro