精华内容
下载资源
问答
  • 用户画像挖掘和应用综合解决方案
  • 2016CCF大数据与计算智能大赛:精准营销中搜狗用户画像挖掘,源码以及PPT分享
  • 2016CCF大数据与计算智能大赛:精准营销中搜狗用户画像挖掘
  • 2016CCF 大数据精准营销中搜狗用户画像挖掘
  • 标准文档 基于 Spark 的大数据精准营销中搜狗搜索引擎的用户画像挖掘 近期参加了 CCF举办的大数据精准营销中搜狗用户画像挖掘竞赛最终得到复赛第 32 名正好这学期机器学习与数据挖掘课程需要一个实验报告的大作业...
  • from CCF举办的“大数据精准营销中搜狗用户画像挖掘”竞赛 1. 选题背景与意义 1.1 用户画像与精准营销 “用户画像”是近几年诞生的名词。很多营销项目或很多广告主,在打算投放广告前,都要求媒体提供其...

    转载请注明:转载 from http://blog.csdn.net/u011239443/article/details/53735609
    from CCF举办的“大数据精准营销中搜狗用户画像挖掘”竞赛

    1. 选题背景与意义

    1.1 用户画像与精准营销

    这里写图片描述
    “用户画像”是近几年诞生的名词。很多营销项目或很多广告主,在打算投放广告前,都要求媒体提供其用户画像。在以前,大多媒体会针对自身用户做一个分类,但是有了大数据后,企业及消费者行为带来一系列改变与重塑,通过用户画像可以更加拟人化的描述用户特点。
    用户画像,即用户信息标签化,就是企业通过收集与分析消费者社会属性、生活习惯、消费行为等主要信息的数据之后,完美地抽象出一个用户的商业全貌,可以看作是企业应用大数据技术的基本方式。用户画像为企业提供了足够的信息基础,能够帮助企业快速找到精准用户群体以及用户需求等更为广泛的反馈信息。
    消费方式的改变促使用户迫切希望尽快获取自己想要了解的信息,所以说,基于用户画像上的精准营销不管对企业还是对用户来说,都是有需求的,这会给双方交易带来极大便捷,也为双方平等沟通搭建了一个畅通平台。

    1.2 搜索引擎下用户画像的挑战

    这里写图片描述
    在搜索引擎下,由于搜索引擎本身使用方式的特殊性、用户的流动性、查询的实时性等,带来了与企业传统的对用户信息进行收集与分析有着巨大的不同、更加艰巨的挑战。
    例如,我们实时获取到的是用户的查询语句,而由于用户的流动性,并不能直接获取到如年龄、性别、学历等用户的标签信息。这么一来,也就无法根据用户属性对用户进行分群处理,而后再通过推荐系统进行产品上的优化

    1.3 本文内容概要

    本文内容概要如下:

    • 第1章:简介用户画像与搜索引擎下用户画像的精准营销的挑战。
    • 第2章:说明实验集群、数据与课题研究目标。
    • 第3章:介绍使用分词工具对用户的搜索词列进行分词,以及相关的优化方案。
    • 第4章:介绍在分词的基础上,对文本进行特征的抽取与转换,以及相关的优化方案。
    • 第5章:介绍在原始特征向量上,进行聚类与降维。
    • 第6章:介绍实验中试验过各分类模型
    • 第7章:介绍模型参数调优
    • 第8章:总结本课题研究中不足与展望后续的优化方案
    • 第9章:参考文献

    2. 课题实验准备

    2.1 Spark集群

    节点备注
    cdh018核,32G内存,角色:Spark Master,HDFS NameNode,Spark Worker,HDFS DataNode
    cdh028核,12G内存,角色:Spark Worker,HDFS DataNode
    cdh038核,12G内存,角色:Spark Worker,HDFS DataNode
    cdh048核,12G内存,角色:Spark Worker,HDFS DataNode

    2.2 数据集

    数据文件备注
    Train.csv带标注的训练集
    Test.csv测试集

    2.3 数据介绍

    本数据来源于搜狗搜索数据,ID经过加密,训练集中人口属性数据存在部分未知的情况(需要解决方案能够考虑数据缺失对算法性能的影响)。数据所有字段如下表所示:

    字段说明
    ID加密后的ID
    age0:未知年龄; 1:0-18岁; 2:19-23岁; 3:24-30岁; 4:31-40岁; 5:41-50岁; 6: 51-999岁
    Gender0:未知1:男性2:女性
    Education0:未知学历; 1:博士; 2:硕士; 3:大学生; 4:高中; 5:初中; 6:小学
    Query List搜索词列表

    2.4 数据示例

    对于train.csv中的数据记录:

    00627779E16E7C09B975B2CE13C088CB 4 2 0 钢琴曲欣赏100首 一个月的宝宝眼睫毛那么是黄色 宝宝右眼有眼屎 小儿抽搐怎么办 剖腹产后刀口上有线头 属羊和属鸡的配吗

    2.5 课题任务描述

    根据提供的用户历史一个月的查询词与用户的人口属性标签(包括性别、年龄、学历)做为训练数据,通过机器学习、数据挖掘技术构建分类算法来对新增用户的人口属性进行判定。

    3. 查询词分词

    3.1 NLPIR

    这里写图片描述
    NLPIR汉语分词系统(又名ICTCLAS2013),主要功能包括中文分词;词性标注;命名实体识别;用户词典功能;支持GBK编码、UTF8编码、BIG5编码。新增微博分词、新词发现与关键词提取;张华平博士先后倾力打造十余年,内核升级10次。
    全球用户突破20万,先后获得了2010年钱伟长中文信息处理科学技术奖一等奖,2003年国际SIGHAN分词大赛综合第一名,2002年国内973评测综合第一名。
    我们传入每个用户的搜索词列,表经过NLPIR分词工具得到的分词。之后,我们做个进一步的优化策略:

    3.1.1 去停用词

    我们根据分词后词语所带的词性,对一些特征代表性不够强的词语进行过滤:

        for (int i = 0; i < sbtmp.length(); ++i) {
            char cc = sbtmp.charAt(i);
            if (cc == ' ') {
                sbtmp.deleteCharAt(i);
                --i;
            } else if (cc == '/') {
    
                // 去词条件
                Boolean isdel =
                        // 1. 去标点
                        (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'w')
                                // 2. 疑问词
                                || (i + 2 < sbtmp.length() && sbtmp.charAt(i + 1) == 'r'
                                        && sbtmp.charAt(i + 2) == 'y')
                                // 3. 数字
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'm')
                                // 4. 连词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'c')
                                // 5. 副词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'd')
                                // 6. 叹词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'e')
                                // 7. 拟声词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'o')
                                // 8. 介词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'p')
                                // 9. 量词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'q')
                                // 10. 助词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'u')
                                // 11. 纯动词
                                || (i + 2 < sbtmp.length() && sbtmp.charAt(i + 1) == 'v'
                                        && sbtmp.charAt(i + 2) == ' ');
    
                // 去词
                if (sbtmp.charAt(i + 1) != 'n' && sbtmp.charAt(i + 1) != 'i' && sbtmp.charAt(i + 1) != 'j'
                        && sbtmp.charAt(i + 1) != 'h'
                        && !(i + 2 < sbtmp.length() && sbtmp.charAt(i + 2) == 'n')) {
                    while (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) != ' ') {
                        sbtmp.deleteCharAt(i + 1);
                    }
                    while (i >= 0 && sbtmp.charAt(i) != ',') {
                        sbtmp.deleteCharAt(i);
                        --i;
                    }
                }
                // 若无需去词,把‘/’转为‘,’,并去除随后的词性标志
                else {
                    sbtmp.setCharAt(i, ',');
                    while (sbtmp.charAt(i + 1) != ' ') {
                        sbtmp.deleteCharAt(i + 1);
                    }
                }
    
            }
        }
        for (int i = 1; i < sbtmp.length() - 1; ++i) {
            if (sbtmp.charAt(i) == ',' && (sbtmp.charAt(i - 1) == ',' || sbtmp.charAt(i + 1) == ',')) {
                sbtmp.deleteCharAt(i);
                --i;
            }
            // 去中间单个字
            else if (sbtmp.charAt(i - 1) == ',' && sbtmp.charAt(i + 1) == ',') {
                sbtmp.deleteCharAt(i);
                sbtmp.deleteCharAt(i);
                --i;
            }
            // 去首个单个字
            else if (sbtmp.charAt(i) == ',' && i == 1) {
                sbtmp.deleteCharAt(i - 1);
                sbtmp.deleteCharAt(i - 1);
                --i;
            }
        }

    3.1.2 提取关键词

    分词并不能很好的将常用的短语提取出来,如词语“用户画像”,使用分词工具更倾向于将其分成“用户”和“画像”,而失去了词语本身的含义。NLPIR还提供了提取一段话的关键词的功能,我们可以使用它:

    int numofIm = 1000;
    String nativeByte = 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 算法的关键词抽取

    import jieba.analyse
    jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())
    1. sentence 为待提取的文本
    2. topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20
    3. withWeight 为是否一并返回关键词权重值,默认值为 False
    4. allowPOS 仅包括指定词性的词,默认值为空,即不筛选
    5. jieba.analyse.TFIDF(idf_path=None) 新建 TFIDF 实例,idf_path 为 IDF 频率文件

    代码示例 (关键词提取)

    import sys
    sys.path.append('../')
    
    import jieba
    import jieba.analyse
    from optparse import OptionParser
    
    USAGE = "usage:    python extract_tags.py [file name] -k [top k]"
    
    parser = OptionParser(USAGE)
    parser.add_option("-k", dest="topK")
    opt, args = parser.parse_args()
    
    
    if len(args) < 1:
        print(USAGE)
        sys.exit(1)
    
    file_name = args[0]
    
    if opt.topK is None:
        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.1 TF-IDF

    • TF(Term Frequency) 词频
    • DF (Document Frequency) 词语出现的文档数目
    • N 总共的文档数目
    • IDF (Invert Document Frequency) 逆文档频率: IDFk=log2NDFk I D F k = l o g 2 N D F k
    • IDF (Invert Document Frequency) 逆文档频率: IDFk=log2(NDFk+0.05) I D F k = l o g 2 ( N D F k + 0.05 )
    • Valuek=TFkIDFk V a l u e k = T F k ∗ I D F k

    IDF反映了一个特征词在整个文档集合中的情况,出现的愈多IDF值越低,这个词区分不同文档的能力越差。
    示例代码:

    import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
    
    val sentenceData = spark.createDataFrame(Seq(
      (0, "Hi I heard about Spark"),
      (0, "I wish Java could use case classes"),
      (1, "Logistic regression models are neat")
    )).toDF("label", "sentence")
    
    val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
    val wordsData = tokenizer.transform(sentenceData)
    val hashingTF = new HashingTF().setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(20)
    val featurizedData = hashingTF.transform(wordsData)
    
    val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
    val idfModel = idf.fit(featurizedData)
    val rescaledData = 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并不是数组,而是一个使用 MurmurHash 3 函数的哈希表。其默认向量维度为 2^18=262,144。我们运行上节示例代码可以发现,我们将哈希表大小设置成了20,第二条sentence:”I wish Java could use case classes”有7个不同的单词,经过hash函数却被映射成了只有5个属性为非零值,即有2个位置放了2个不同的单词。这具有很大的随机性,将两个无关词义的词语,甚至词义相反的词语,如“男”与“女”,映射到哈希表的同一位置,作为相同的用户属性来处理。

    4.2 CountVectorizer

    为了解决上节所提到的HashingTF哈希函数映射后导致词语重叠问题,我们使用了Spark的CountVectorizer。我们会先想CountVectorizer传入一个互斥的字符串数组,文本经过CountVectorizer转换后,会对该数组中所有的词语进行与属性的一一对应。
    我们对互斥的字符串数组进行的优化,过滤掉了词频为1的词语,将CountVectorizer的维度减少到原来的50%,极大的降低了后续训练模型时所需的内存,而且除去的数据噪音,增加了预测的准确度:

    val diffTrain = Triandata.map { line =>
          val temp = line.split("\t")
          if (temp.length == 5) temp(4) else ""
        }
    val diffTest = Testdata.map { line =>
          val temp = line.split("\t")
          if (temp.length == 5) temp(1) else ""
        }
    val diffAll = diffTrain.union(diffTest).flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _).collect.filter(line => line._1 != "" && line._2 > 14).map(line => line._1)
    val cvm = new CountVectorizerModel(diffAll).setInputCol(tokenizer.getOutputCol).setOutputCol("features")  

    4.3 StopWordsRemover

    这里写图片描述
    在上一章中,我们提到了分词时,根据分词结果所带的词性,对其进行去停用词。而后,我们发现使用”结巴”分词进行TF-IDF算法对用户搜索词列进行1000个关键词的提取对于后续的分类模型效果会更好。但是,我们在“结巴”关键词提取的结果却发现了类似于“什么”“即使”等不具有代表性的词语。于是我们1119个停用词,使用Spark的StopWordsRemover,对分词结果进行去停用词:

    val Stopdata = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/stop",128).collect()
    val remover = new StopWordsRemover().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]。
    Rescaled(ei)=eiEminEmaxEmin(maxmin)+min R e s c a l e d ( e i ) = e i − E m i n E m a x − E m i n ∗ ( m a x − m i n ) + m i n
    Emin,Emax E m i n , E m a x 是某个特征向量所有元素的最大最小值
    max,min是用户可以重新自定义的范围,默认为【0,1】,由所有特征共享

    在后续,当我们对特征矩阵进行聚类后,得到的特征值可能为负值,可是很多分类器模型需要特征值为非负值。使用以上方法也可以解决这个问题。

    4.5 同义词替换

    这里写图片描述
    设想当一个用户的搜索词列的分词结果中出现了一些意思相近的词语,如“恋爱”与“爱情”、“菠萝”与“凤梨”。而我们的模型将其辨别为不同的特征属性,这无疑大量的增加了特征向量的维度和平分了同一意思的词语具有的代表性。
    为了解决上述问题,我们搜集了近4万条同义词词典,将意思相近的词语由1个词语来替换掉。该优化帮助原本的特征向量减少了3万以上的维度,降低了后续训练模型时所需的内存,而且凝聚了属性的代表性,增加了预测的准确度:

        val sqlContext = new org.apache.spark.sql.SQLContext(sc)
        import sqlContext.implicits._
        val train = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/JBtrain", 400)
        val test = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/JBtest", 400)
        val same = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/same", 400)
        same.filter { x => !x.contains('=') }.count()
        val sameWord = same.map { line =>
          val valuekey = line.split('=')
          (valuekey(1), valuekey(0))
        }.collect()
        val broadcastVar = sc.broadcast(sameWord)
        val diffTrain = train.map { line =>
          val broad = broadcastVar.value
          val regex = """^\d+$""".r
          val temp = line.split("\t")
          val wordArray = temp(4).split(",")
          var str = ""
          for (word <- wordArray) {
            val keyVal = broad.filter(line => line._1.equals(word))
            if (keyVal.length > 0) {
              val oneKeyVal = keyVal(0)
              str = str + "#" + oneKeyVal._2
            } else if (regex.findFirstMatchIn(word) == None) {
              str = str + "#" + word
            }
          }
          (temp(0), temp(1), temp(2), temp(3), str)
        }
        diffTrain.toDF().coalesce(1).write.csv("hdfs://cdh01:8020//user/data/sogou2/ReplaceJBtrain")
    
        val diffTest = test.map { line =>
          val broad = broadcastVar.value
          val regex = """^\d+$""".r
          val temp = line.split("\t")
          val wordArray = temp(1).split(",")
          var str = ""
          for (word <- wordArray) {
            val keyVal = broad.filter(line => line._1.equals(word))
            if (keyVal.length > 0) {
              val oneKeyVal = keyVal(0)
              str = str + "#" + oneKeyVal._2
            } else if (regex.findFirstMatchIn(word) == None) {
              str = str + "#" + word
            }
          }
          (temp(0), str)
        }
        diffTest.toDF().coalesce(1).write.csv("hdfs://cdh01:8020//user/data/sogou2/ReplaceJBtest")

    4.6 形近词替换

    这里写图片描述
    设想当一个用户的搜索词列的分词结果中出现了一些形近的词语,如“iPhone5”与“iPhone6”、“杭州”与“杭州站”。该问题和上节所提出的问题类似,为了解决这个问题,我们先来介绍“编辑距离算法”。
    编辑距离算法,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如,计算X字符串 与 Y字符串 的 编辑距离。dp[i][j] 为 X串的前i个字符 和 Y串的前j个字符 的编辑距离:

    if(X [i - 1] == Y[j - 1])       
        dp[i][j] = dp[i - 1][j - 1];   //最后字符相同    
    else
    { 
        int t1 = dp[i - 1][j];                             //删除X第i个字符    
        t1 = t1 < dp[i][j - 1] ? t1 : dp[i][j - 1];        //删除Y第j个字符    
        t1 = t1 < dp[i - 1][j - 1] ? t1 : dp[i - 1][j - 1];//最后字符改相同
        dp[i][j] = t1 + 1;
    }

    这里我们所使用的优化方案为:
    这里写图片描述

    • 对整个训练集和测试集的搜索词列做分词后的词频统计表
    • 对每个用户的搜索词列分词后的各个词与词频统计表各词(排除前者自身)进行编辑距离计算。
    • 得到词频统计表中编辑距离与该词编辑距离最小词,在这些词中在选择一个词频最高的词将该词替代。

    4.7 额外增加数据量

    这里写图片描述
    在大数据时代背景下,只要数据量足够的大,反而我们所选用的不同的算法模型对最终的预测准确率的影响会变小,获取更多数据会使模型更完善更准确。我们这里用不同方案所得到的分词结果,人为的增加训练集的数据。如将10万条记录的训练集进行NLPIR分词得到结果,与进行”结巴”提取关键词得到的结果拼接,就将训练集记录人为的翻倍了。后续的分类实验中证明了,使用该方案,在模型相同的情况下,相比原来会有1%左右的准确率的提升。

    5.聚类与降维

    2009年结束的Nexfix竞赛表明,很多参数团队用到的高等矩阵因子分解对模型提高预测准确略非常有帮助。模型使用矩阵因子分解方法从特征矩阵中抽取一组潜在的属性,并通过这些属性来描述用户。20世纪80年代后期,利用潜在的”语义”属性的思想被成功的应用于信息检索领域。Deerwesteret al. 在1990年提出使用奇异值分解(SVD)方法发现文档中的潜在的属性。[2]而本课题在实验中会使用到LDA方法。

    5.1 LDA

    隐含狄利克雷分配(LDA,Latent Dirichlet Allocation)是一种主题模型(Topic Model,即从所收集的文档中推测主题)。 甚至可以说LDA模型现在已经成为了主题建模中的一个标准,是实践中最成功的主题模型之一。那么何谓“主题”呢?,就是诸如一篇文章、一段话、一个句子所表达的中心思想。不过从统计模型的角度来说, 我们是用一个特定的词频分布来刻画主题的,并认为一篇文章、一段话、一个句子是从一个概率模型中生成的。也就是说 在主题模型中,主题表现为一系列相关的单词,是这些单词的条件概率。形象来说,主题就是一个桶,里面装了出现概率较高的单词(参见下面的图),这些单词与这个主题有很强的相关性。
    这里写图片描述
    LDA可以用来识别大规模文档集或语料库中潜藏的主题信息。它采用了词袋的方法,这种方法将每一篇文档视为一个词频向量,从而将文本信息转化为了易于建模的数字信息。但是词袋方法没有考虑词与词之间的顺序,这简化了问题的复杂性,同时也为模型的改进提供了契机。每一篇文档代表了一些主题所构成的一个概率分布,而每一个主题又代表了很多单词所构成的一个概率分布。
    LDA可以被认为是如下的一个聚类过程:

    • 各个主题(Topics)对应于各类的“质心”,每一篇文档被视为数据集中的一个样本。
    • 主题和文档都被认为存在一个向量空间中,这个向量空间中的每个特征向量都是词频(词袋模型)
    • 与采用传统聚类方法中采用距离公式来衡量不同的是,LDA使用一个基于统计模型的方程,而这个统计模型揭示出这些文档都是怎么产生的。

    5.1.1 模型训练

    Spark API 参数介绍:

    • K:主题数量(或者说聚簇中心数量)
    • maxIterations:EM算法的最大迭代次数,设置足够大的迭代次数非常重要,前期的迭代返回一些无用的(极其相似的)话题,但是继续迭代多次后结果明显改善。我们注意到这对EM算法尤其有效。,至少需要设置20次的迭代,50-100次是更合理的设置,取决于数据集。
    • docConcentration(Dirichlet分布的参数α):文档在主题上分布的先验参数(超参数α)。当前必须大于1,值越大,推断出的分布越平滑。默认为-1,自动设置。
    • topicConcentration(Dirichlet分布的参数β):主题在单词上的先验分布参数。当前必须大于1,值越大,推断出的分布越平滑。默认为-1,自动设置。
    • checkpointInterval:检查点间隔。maxIterations很大的时候,检查点可以帮助减少shuffle文件大小并且可以帮助故障恢复。
        val lda=new LDA()
                    .setK(20)
                    .setOptimizer("online")
                    .setCheckpointInterval(10)
                    .setMaxIter(100)
        val model=lda.fit(dataset_lpa)   

    5.1.2 模型评价

    生成的model不仅存储了推断的主题,还包括模型的评价方法。模型的评价指标:logLikelihood,logPerplexity。logLikelihood越大越好,logPerplexity越小越好

        val ll = model.logLikelihood(dataset_lpa)
        val lp = model.logPerplexity(dataset_lpa)

    用评价方法,在online 方法下,对setMaxIter进行调参:

    for(i<-Array(5,10,20,40,60,120,200,500)){
        val lda=new LDA()
                    .setK(3)
                    .setTopicConcentration(3)
                    .setDocConcentration(3)
                    .setOptimizer("online")
                    .setCheckpointInterval(10)
                    .setMaxIter(i)
        val model=lda.fit(dataset_lpa) 
    
        val ll = model.logLikelihood(dataset_lpa) 
        val lp = model.logPerplexity(dataset_lpa)
    
        println(s"$i $ll")
        println(s"$i $lp")
     }

    可以看到,logPerplexity在减小,LogLikelihood在增加,最大迭代次数需要设置50次以上,才能收敛:
    这里写图片描述
    这里写图片描述

    5.1.3 对语料的主题进行聚类

        val topicsProb=model.transform(dataset_lpa)
        topicsProb.select("label", "topicDistribution")show(false)
        /** 
            +-----+--------------------------------------------------------------+
            |label|topicDistribution                                             |
            +-----+--------------------------------------------------------------+
            |0.0  |[0.523730754859981,0.006564444943344147,0.46970480019667477]  |
            |1.0  |[0.7825074858166653,0.011001204994496623,0.206491309188838]   |
            |2.0  |[0.2085069748527087,0.005698459472719417,0.785794565674572]   |
            ...
    
        */

    label是文档序号,文档中各主题的权重,我们可以将该DataFrame带入后续的分类器中,进行训练。

    5.1.4 其他聚类与降维

    Spark在基于RDD的MLlib中还提供了SVD、PCA的降维方法,而基于DataFrame的聚类方法还包括k-means、Bisecting k-means和Gaussian Mixture,其中Gaussian Mixture提供的API类似与LDA,可以直接为我们返回文档中各主题的权重,以便于后续的分类。但是由于LDA在主题聚类上的典型性,我们的课题实验只试验了LDA的方案。

    6. 分类方案

    6.1 Cosine相似度

    假设现在我们有一个测试集特征向量A和一个训练集的特征向量B:

    A:[1, 2, 2, 1, 1, 1, 0]
    B:[1, 2, 2, 1, 1, 2, 1]

    到这里,问题就变成了如何计算这两个向量的相似程度。我们可以把它们想象成空间中的两条线段,都是从原点([0, 0, …])出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。

    这里写图片描述

    以二维空间为例,上图的a和b是两个向量,我们要计算它们的夹角θ。余弦定理告诉我们,可以用下面的公式求得:
    cosΘ=x1x2+y1y2x21+y21x22+y22 c o s Θ = x 1 x 2 + y 1 y 2 x 1 2 + y 1 2 ∗ x 2 2 + y 2 2

    拓展到n维向量,假定A和B是两个n维向量,A是 [A1, A2, …, An] ,B是 [B1, B2, …, Bn] ,则A与B的夹角θ的余弦等于:
    cosΘ=Σni=1(AiBi)Σni=1(Ai)2Σni=1(Bi)2=AB|A||B| c o s Θ = Σ i = 1 n ( A i ∗ B i ) Σ i = 1 n ( A i ) 2 ∗ Σ i = 1 n ( B i ) 2 = A ⋅ B | A | ∗ | B |

    使用这个公式,我们就可以得到,特征向量A与特征向量B的夹角的余弦:
    cosΘ=11+22+22+11+11+12+0112+22+22+12+12+12+0212+22+22+12+12+22+12=131216=0.938 c o s Θ = 1 ∗ 1 + 2 ∗ 2 + 2 ∗ 2 + 1 ∗ 1 + 1 ∗ 1 + 1 ∗ 2 + 0 ∗ 1 1 2 + 2 2 + 2 2 + 1 2 + 1 2 + 1 2 + 0 2 ∗ 1 2 + 2 2 + 2 2 + 1 2 + 1 2 + 2 2 + 1 2 = 13 12 ∗ 16 = 0.938

    余弦值越接近1,就表明夹角越接近0度,也就是两个向量越相似,这就叫”余弦相似度”
    我们这个方案,计算出一条测试集的特征向量与训练集各个特征向量的余弦相似度,将该条测试集的类别标记为与其余弦相似度最大的训练集特征向量所对应的类别。

    6.2 One-vs-Rest

    One-vs-Rest将只能用于二分问题的分类(如Logistic回归、SVM)方法扩展到多类。这种方法基本思想为:

    训练时依次把某个类别的样本归为一类,其他剩余的样本归为另一类,这样k个类别的样本就构造出了k个binary分类器。分类时将未知样本分类为具有最大分类函数值的那类。
    假如我有四类要划分(也就是4个Label),他们是A、B、C、D。于是在抽取训练集的时候,分别抽取
    (1)A所对应的向量作为正集,B,C,D所对应的向量作为负集
    (2)B所对应的向量作为正集,A,C,D所对应的向量作为负集;
    (3)C所对应的向量作为正集,A,B,D所对应的向量作为负集;
    (4)D所对应的向量作为正集,A,B,C所对应的向量作为负集;
    使用这四个训练集分别进行训练,然后的得到四个训练结果文件。在测试的时候,把对应的测试向量分别利用这四个训练结果文件进行测试。最后每个测试都有一个结果f1(x),f2(x),f3(x),f4(x)。于是最终的结果便是这四个值中最大的一个作为分类结果。

    代码实例:

    //定义一个binary分类器,如:LogisticRegression 
    LogisticRegression lr=new LogisticRegression()
                    .setMaxIter(10)
                    .setRegParam(0.3)
                    .setElasticNetParam(0.2)                
                    .setThreshold(0.5);
    //建立一对多多分类器model                
    OneVsRestModel model=new OneVsRest()
                    .setClassifier(lr)//将binary分类器用这种办法加入
                    .fit(training);
    //利用多分类器model预测
    Dataset<Row>predictions=model.transform(test);  

    很遗憾的是,目前Spark基于DataFrame的MLlib binary分类器中并没有实现SVM,而基于RDD的MLlib有实现SVM,却没有实现One-vs-Rest。

    6.3 朴素贝叶斯

    朴素贝叶斯分类是一种思想比较简单的分类算法,朴素贝叶斯的思想基础是这样的:对于给出的待分类项,求解在此项出现的条件下各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。
    朴素贝叶斯分类的正式定义如下:

    1、设为一个待分类测试特征向量 x=a1,a2,...,am x = a 1 , a 2 , . . . , a m ,而每个 ai a i 为属性 xi x i 的值。

    2、有类别集合 C=y1,y2,...,yn C = y 1 , y 2 , . . . , y n

    3、计算 P(y1|x),P(y2|x)...P(yn|x) P ( y 1 | x ) , P ( y 2 | x ) . . . P ( y n | x )

    4、将该待分类的用户类别标记为 yk y k

    P(yk|x)=max(P(y1|x),P(y2|x)...P(yn|x)),k(1..n) P ( y k | x ) = m a x ( P ( y 1 | x ) , P ( y 2 | x ) . . . P ( y n | x ) ) , k ∈ ( 1.. n )

    那么现在的关键就是如何计算第3步中的各个条件概率。我们假设各个特征属性是条件独立的,则根据贝叶斯定理有如下推导:

    P(yi|x)=P(x|yi)P(yi)P(x) P ( y i | x ) = P ( x | y i ) P ( y i ) P ( x )

    因为分母对于所有类别为常数,因为我们只要将分子最大化皆可。又因为各特征属性是条件独立的,所以有:

    P(x|yi)P(yi)=P(a1|yi)P(a2|yi)...P(am|pi)P(yi) P ( x | y i ) P ( y i ) = P ( a 1 | y i ) P ( a 2 | y i ) . . . P ( a m | p i ) P ( y i )

    而等式右边的各项都可以有训练集求出。以此类推,便可以计算出计算第3步。
    Spark Mllib实现朴素贝叶斯分类器,Smoothing为平滑系数:

    val lr = new NaiveBayes().setSmoothing(0.1).setFeaturesCol("features")

    6.4 前馈神经网络

    Spark MLlib中实现了MultilayerPerceptronClassifier(MLPC),这是一个基于前馈神经网络的分类器,它是一种在输入层与输出层之间含有一层或多层隐含结点的具有正向传播机制的神经网络模型。

    这里写图片描述

    中间的节点使用sigmoid (logistic)函数,输出层的节点使用softmax函数。输出层的节点的数目表示分类器有几类。MLPC学习过程中使用BP算法,优化问题抽象成logistic loss function并使用L-BFGS进行优化。
    下面我们来介绍下MultilayerPerceptronClassifier所设计到的参数:

    val lr = new MultilayerPerceptronClassifier().setMaxIter(51).setLayers(Array[Int](vector_len, 6, 5, classfiy_num)).setSeed(1234L).setFeaturesCol("features").setLabelCol("label").setPredictionCol("prediction")
    • layers:各层的节点数,第1层的个数必须为特征向量的维度数,最后1层的个数必须为类别数,中间为各隐藏层的节点数,可以任意设置。对于隐藏成节点数的优化,我们做了一下的方案:由于我们的年龄、性别、学历的类别数分别是6、2、6。假设我们在预测年龄时,我们可以将隐藏的第1层节点数设为6 * 2 * 6 = 72,即根据先将其分为3个label都互斥的类别,。然后,将隐藏的第2层节点数设为6 * 2 = 12,即根据先将其分为年龄和性别3个label都互斥的类别。最后,再分到我们想预测的年龄的6个类别上。
    • maxIter:最大迭代次数。进行参数调优时,主要是在优化这个参数。
    • Seed:随机种子
    • tol:(代码中未设置)允许误差。一般取0.001~0.00001,当迭代结果的误差小于该值时,结束迭代计算,给出结果。
    • solver:(代码中未设置)优化器。有两种算法可供选择: l-bfgs和gd。默认为 l-bfgs算法,因为gd算法比l-bfgs算法需要多得多的迭代次数,即使在提高学习步长和提高允许误差tol的情况下,还是慢很多:

      .stepSize=0.03,tol=0.0001
      l-bfgs:上很快能收敛,大约20次,训练速度也更快
      maxIter = 5 accuracy = 0.35 training time = 267ms
      maxIter = 10 accuracy = 0.74 training time = 429ms
      maxIter = 20 accuracy = 0.91 training time = 657ms
      maxIter = 50 accuracy = 0.92 training time = 941ms
      maxIter = 100 accuracy = 0.92 training time = 914ms
      maxIter = 500 accuracy = 0.92 training time = 1052ms

      这里写图片描述

      gd算法:基本上要比l-bfgs算法慢10以上
      stepsize=0.2,tol=0.001
      maxIter = 100 accuracy = 0.55 training time = 4209ms
      maxIter = 500 accuracy = 0.92 training time = 11216ms
      maxIter = 1000 accuracy = 0.92 training time = 14540ms
      maxIter = 2000 accuracy = 0.92 training time = 14708ms
      maxIter = 5000 accuracy = 0.92 training time = 14669ms

    • stepSize:学习步长。一般来说学习步长越大,权重变化越大,收敛越快;但训练速率过大,会引起系统的振荡。太高的学习步长,可以减少网络训练的时间,但是容易导致网络的不稳定与训练误差的增加,会引起系统的振荡。

    6.5 预分类迭代预测

    这里写图片描述
    数据表明,当我们在进行对年龄进行分类时,其实同一年龄段的男性和女性查询语句是存在较大的差异的。我们可以借鉴6.4 前馈神经网络中对layers(各层的节点数)优化的类似的思想:
    1. 将测试集进行性别男女的预测分类,预测成男性的分为测试集test1,预测成女性的分为测试集test2。
    2. 将训练集根据性别划分为train1(男性)和train2(女性)。
    3. 分别用train1训练模型来预测test1的年龄,train2训练模型来预测test2年龄。
    上诉思想可以实现一种迭代,即继续对年龄的预测结果进行划分来预测学历,再对学历的预测结果进行划分来预测性别,在进行上诉的第2、第3步,如此反复继续。
    这个方案由于将数据集划分了,每次降低了数据量,这会对准确率有负面的影响,但最终总体预测准确率提高了0.3%左右。

    6.6 Boosting

    Boosting分类器属于集成学习模型,它基本思想是把成百上千个分类准确率较低的树模型组合起来,成为一个准确率很高的模型。这个模型会不断地迭代,每次迭代就生成一颗新的树。

    6.6.1 xgboost

    xgboost 的全称是eXtreme Gradient Boosting。它是Gradient Boosting Machine的一个c++实现,作者为华盛顿大学研究机器学习专家陈天奇 。他在研究中深感自己受制于现有库的计算速度和精度,因此开始着手搭建xgboost项目。xgboost最大的特点在于,它能够自动利用CPU的多线程进行并行,同时在算法上加以改进提高了精度。在Kaggle的希格斯子信号识别竞赛中,它因为出众的效率与较高的预测准确度在比赛论坛中引起了参赛选手的 广泛关注 ,在1700多支队伍的激烈竞争中占有一席之地。
    xgboost拥有自身的jvm项目包,可以和Spark集成。很遗憾的是,由于时间关系,我们并没有成功尝试这个方案,而是在已有分类的结果上实现了简单的加权投票的策略,最终总体预测准确率提高了0.4%左右。

    7. 参数调优

    7.1 交叉验证法

    Spark Mllib 中实现的是留一法交叉验证法。留一法交叉验证法的思想是:将原来的训练集有N个数据集,将每一个数据集作为测试集,其它N-1个数据集作为训练集。这样得到N个分类器,N个测试结果。用这N个结果的平均值来衡量模型的性能:

    val pipeline = new Pipeline()
      .setStages(Array(tokenizer, hashingTF, lr))
    
    val paramGrid = new ParamGridBuilder()
      .addGrid(hashingTF.numFeatures, Array(10, 100, 1000))
      .addGrid(lr.regParam, Array(0.1, 0.01))
      .build()
    
    val cv = new CrossValidator()
      .setEstimator(pipeline)
      .setEvaluator(new BinaryClassificationEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setNumFolds(2)  
    
    val cvModel = cv.fit(training)
    
    ......
    
    cvModel.transform(test)

    参数介绍:

    • Estimator:即所要进行评估的模型
    • Evaluator:对模型评估器,可以是二分类的BinaryClassificationEvaluator 或是 多分类的 MulticlassClassificationEvaluator
    • EstimatorParamMaps:模型参数表。可以由ParamGridBuilder调用addGrid方法对模型的某个参数设置一组需要验证的值,再调用build()返回。
    • NumFolds:即所要划分的数据集的数量N

    7.2 划分训练集验证法

    划分训练集验证法的思想比较简单,我们将训练集按 m:1 - m 的比例划分成两个部分,第1部分作为新的训练集,第2部分作为验证集:

    val trainValidationSplit = new TrainValidationSplit()
      .setEstimator(lr)
      .setEvaluator(new RegressionEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setTrainRatio(0.8)

    我们可以从上述代码看到参数基本上和交叉验证法相同,其中TrainRatio就是我们说的m。
    划分训练集验证法优点在于所需要的时间较短。当我们在对前馈神经网络参数调优时,因为耗时过长而无法选用交叉验证法,而划分训练集验证法则是一种很好的替代方案。
    很遗憾的是,Spark Mllib所实现的交叉验证法和划分训练集验证法都没有返回验证所选得的一组最优参数的API,而是将其视为一种模型直接对原始训练集进行训练,最后返回预测结果。而且,划分训练集验证法只对训练集划分一次进行预测,这具有很大的偶然性。由于以上的原因,我们动手自己实现了划分训练集验证法,并每次验证进行了三次的随机划分和训练,以其平均值作为验证的结果,最后按准确率对参数组降序排序。:

       val evaluations =
          for (
            smooth <- 1 to 100
          ) yield {
            val lr = new NaiveBayes().setSmoothing(smooth.toDouble / 100.0).setFeaturesCol("features")
            val pipeline = new Pipeline().setStages(Array(lr))
            var sumA = 0.0
            for (cnt <- 1 to 3) {
              val Array(trainData, testData) = dataIDF.randomSplit(Array(0.7, 0.3))
              trainData.cache()
              testData.cache()
              val model = pipeline.fit(trainData)
              val predictions = model.transform(testData)
              val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction").setMetricName("accuracy")
              val accuracy = evaluator.evaluate(predictions)
              sumA = sumA + accuracy
            }
            val allAccuracy = sumA / 3.0
            println(((smooth), allAccuracy))
            ((smooth), allAccuracy)
          }
        evaluations.sortBy(_._2).reverse.foreach(println)

    使用上述代码对贝叶斯分类器关于各个label预测的参数(平滑度)调优,总体平均准确率相比原来有6%左右的提升。

    8. 回顾与展望

    8.1 总结

    经过我们对各个分词、特征提取与转换、聚类、分类以及参数调优的方案的尝试,最终我们选择了:

    • 分词:NLPIR分词、结巴TF-IDF与结巴TextRank叠加,将训练集数据量变为原来的3倍。
    • 特征提取与转换:选择了HashingTF,维度设置为10万。
    • 聚类:弃用。
    • 分类:使用前馈神经网络,对每个Label预测,设置参数都为:
    MultilayerPerceptronClassifier().setMaxIter(32).setLayers(Array[Int](vector_len, 6, 5, classfiy_num))
    
    • 参数调优:将HashingTF维度降低为1万,使用划分训练集验证法进行调优。

    该方案所能达到的平均准确率为69.652%,再对其他方案与该方案进行简单的加权投票,平均准确率可达70%左右。

    8.2 不足与展望

    从上节可以发现,很遗憾的我们在最终的方案中做了很多原本优化上的妥协:

    • 分词:从最终分词结果的数据上表明,分词结果还存在非常大可以改进的地方,还存在许多代表性极弱的词语。
    • 特征提取与转换:由于模型的选择,试验证明使用IDF和权值规范化,并不能得到预期的效果。反而,只使用CountVectorizer会得到较好的效果。而在选用前馈神经网络分类器后,Spark机器无法支撑CountVectorizer如此大的维度(按结巴TF-IDF分词结果,使用CountVectorizer会得到维度为60多万的特征向量,而去词频为1的词语后得到的特征向量维度为30多万),因而我们退而求其次选用了HashingTF。
    • 聚类:实验中的Spark集群使用LDA,所能承受特征维度在30万以下,影响了LDA的效果。而训练一次(迭代100次)所花费的时间,我们实在无法承受,再而我们对参数的设置(如主题个数、迭代次数)并没太大的经验。在多次试验,得到后续的分类效果并不良好的情况下,我们放弃了这个方案。
    • 分类与参数调优:当我们弃用朴素贝叶斯分类器,转而试用并未进行参数调优的前馈神经网络分类器后,惊喜的发现后者比前者参数调优过的平均预测准确率还要高出4%左右。但很遗憾的是,就算我们将特征向量维度降低至10万,对前馈神经网络分类器的参数调优所耗费的时间还是远远的超出我们所能承受的。于是,我们尝试将维度降低至1万,调优得到参数组后,再带回到10万维度的特征向量;尝试减少数据量进行参数调优。而结果证明,这两种方案的调优都不能与使用原始的特征向量调优效果一致。
    • xgboost:由于时间关系,我们并没有成功尝试这个方案,而是在已有分类的结果上实现了简单的加权投票的策略。

    由于我们的经验不足,再加上实验室的项目任务、在校课程的学习任务以及后期转而对Spark Core源码的研究,使得原本可以做的优化并没有精力去很好的实现。希望在后续能将其完善的更好。

    8.3 感谢

    感谢实验室所提供的服务器集群环境,感谢同组同学在一起交流讨论中激发出灵感。非常感谢这次课题实验给我带来的学习机会,让我从头到尾自主的完成了一次数据处理、分析的过程,也深深的感受到了Spark的魅力和大数据处理的重要性,也坚定了我从事Spark大数据处理与分析研究的决心。

    9. 参考文献

    【1】Mihalcea R, Tarau P. TextRank: Bringing order into texts[C]. Association for Computational Linguistics, 2004.
    【2】Dietmar J, Markus Z. Recommender systems: An introduction[M]. Cambridge University Press, 2010: 35.

    这里写图片描述

    展开全文
  • text classfication 大数据精准营销中搜狗用户画像挖掘 rank61/880 2016-ccf-data-mining-competition 大数据精准营销中搜狗用户画像挖掘 竞赛简介 在现代广告投放系统中,多层级成体系的用户画像构建算法是实现精准...
  • 【正确的团队-原始码以及PPT分享】2016CCF大数据与计算智能大赛:精准营销中搜狗用户画像挖掘 具体详见我的博客: 复赛数据下载链接: ://pan.baidu.com/s/1mi9DjIg密码:g8i9 初识python,代码写的很粗糙,多多...
  • 近期参加了CCF举办的“大数据精准营销中搜狗用户画像挖掘”竞赛,就那它来写了。本博文会在这几周不断的完善更新ing 1. 选题背景与意义 1.1 用户画像与精准营销   “用户画像”是近几年诞生的名词。很多...

    转载请注明:转载 from http://blog.csdn.net/u011239443/article/details/53735609 
    近期参加了CCF举办的“大数据精准营销中搜狗用户画像挖掘”竞赛,就那它来写了。本博文会在这几周不断的完善更新ing

    1. 选题背景与意义

    1.1 用户画像与精准营销

    这里写图片描述 
    “用户画像”是近几年诞生的名词。很多营销项目或很多广告主,在打算投放广告前,都要求媒体提供其用户画像。在以前,大多媒体会针对自身用户做一个分类,但是有了大数据后,企业及消费者行为带来一系列改变与重塑,通过用户画像可以更加拟人化的描述用户特点。 
    用户画像,即用户信息标签化,就是企业通过收集与分析消费者社会属性、生活习惯、消费行为等主要信息的数据之后,完美地抽象出一个用户的商业全貌,可以看作是企业应用大数据技术的基本方式。用户画像为企业提供了足够的信息基础,能够帮助企业快速找到精准用户群体以及用户需求等更为广泛的反馈信息。 
    消费方式的改变促使用户迫切希望尽快获取自己想要了解的信息,所以说,基于用户画像上的精准营销不管对企业还是对用户来说,都是有需求的,这会给双方交易带来极大便捷,也为双方平等沟通搭建了一个畅通平台。

    1.2 搜索引擎下用户画像的挑战

    这里写图片描述 
    在搜索引擎下,由于搜索引擎本身使用方式的特殊性、用户的流动性、查询的实时性等,带来了与企业传统的对用户信息进行收集与分析有着巨大的不同、更加艰巨的挑战。 
    例如,我们实时获取到的是用户的查询语句,而由于用户的流动性,并不能直接获取到如年龄、性别、学历等用户的标签信息。这么一来,也就无法根据用户属性对用户进行分群处理,而后再通过推荐系统进行产品上的优化

    1.3 本文内容概要

    本文内容概要如下:

    • 第1章:简介用户画像与搜索引擎下用户画像的精准营销的挑战。
    • 第2章:说明实验集群、数据与课题研究目标。
    • 第3章:介绍使用分词工具对用户的搜索词列进行分词,以及相关的优化方案。
    • 第4章:介绍在分词的基础上,对文本进行特征的抽取与转换,以及相关的优化方案。
    • 第5章:介绍在原始特征向量上,进行聚类与降维。
    • 第6章:介绍实验中试验过各分类模型
    • 第7章:介绍模型参数调优
    • 第8章:总结本课题研究中不足与展望后续的优化方案
    • 第9章:参考文献

    2. 课题实验准备

    2.1 Spark集群

    节点 备注
    cdh01 8核,32G内存,角色:Spark Master,HDFS NameNode,Spark Worker,HDFS DataNode
    cdh02 8核,12G内存,角色:Spark Worker,HDFS DataNode
    cdh03 8核,12G内存,角色:Spark Worker,HDFS DataNode
    cdh04 8核,12G内存,角色:Spark Worker,HDFS DataNode

    2.2 数据集

    数据文件 备注
    Train.csv 带标注的训练集
    Test.csv 测试集

    2.3 数据介绍

    本数据来源于搜狗搜索数据,ID经过加密,训练集中人口属性数据存在部分未知的情况(需要解决方案能够考虑数据缺失对算法性能的影响)。数据所有字段如下表所示:

    字段 说明
    ID 加密后的ID
    age 0:未知年龄; 1:0-18岁; 2:19-23岁; 3:24-30岁; 4:31-40岁; 5:41-50岁; 6: 51-999岁
    Gender 0:未知1:男性2:女性
    Education 0:未知学历; 1:博士; 2:硕士; 3:大学生; 4:高中; 5:初中; 6:小学
    Query List 搜索词列表

    2.4 数据示例

    对于train.csv中的数据记录:

    00627779E16E7C09B975B2CE13C088CB 4 2 0 钢琴曲欣赏100首 一个月的宝宝眼睫毛那么是黄色 宝宝右眼有眼屎 小儿抽搐怎么办 剖腹产后刀口上有线头 属羊和属鸡的配吗

    2.5 课题任务描述

    根据提供的用户历史一个月的查询词与用户的人口属性标签(包括性别、年龄、学历)做为训练数据,通过机器学习、数据挖掘技术构建分类算法来对新增用户的人口属性进行判定。

    3. 查询词分词

    3.1 NLPIR

    这里写图片描述 
    NLPIR汉语分词系统(又名ICTCLAS2013),主要功能包括中文分词;词性标注;命名实体识别;用户词典功能;支持GBK编码、UTF8编码、BIG5编码。新增微博分词、新词发现与关键词提取;张华平博士先后倾力打造十余年,内核升级10次。 
    全球用户突破20万,先后获得了2010年钱伟长中文信息处理科学技术奖一等奖,2003年国际SIGHAN分词大赛综合第一名,2002年国内973评测综合第一名。 
    我们传入每个用户的搜索词列,表经过NLPIR分词工具得到的分词。之后,我们做个进一步的优化策略:

    3.1.1 去停用词

    我们根据分词后词语所带的词性,对一些特征代表性不够强的词语进行过滤:

        for (int i = 0; i < sbtmp.length(); ++i) {
            char cc = sbtmp.charAt(i);
            if (cc == ' ') {
                sbtmp.deleteCharAt(i);
                --i;
            } else if (cc == '/') {
    
                // 去词条件
                Boolean isdel =
                        // 1. 去标点
                        (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'w')
                                // 2. 疑问词
                                || (i + 2 < sbtmp.length() && sbtmp.charAt(i + 1) == 'r'
                                        && sbtmp.charAt(i + 2) == 'y')
                                // 3. 数字
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'm')
                                // 4. 连词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'c')
                                // 5. 副词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'd')
                                // 6. 叹词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'e')
                                // 7. 拟声词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'o')
                                // 8. 介词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'p')
                                // 9. 量词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'q')
                                // 10. 助词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'u')
                                // 11. 纯动词
                                || (i + 2 < sbtmp.length() && sbtmp.charAt(i + 1) == 'v'
                                        && sbtmp.charAt(i + 2) == ' ');
    
                // 去词
                if (sbtmp.charAt(i + 1) != 'n' && sbtmp.charAt(i + 1) != 'i' && sbtmp.charAt(i + 1) != 'j'
                        && sbtmp.charAt(i + 1) != 'h'
                        && !(i + 2 < sbtmp.length() && sbtmp.charAt(i + 2) == 'n')) {
                    while (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) != ' ') {
                        sbtmp.deleteCharAt(i + 1);
                    }
                    while (i >= 0 && sbtmp.charAt(i) != ',') {
                        sbtmp.deleteCharAt(i);
                        --i;
                    }
                }
                // 若无需去词,把‘/’转为‘,’,并去除随后的词性标志
                else {
                    sbtmp.setCharAt(i, ',');
                    while (sbtmp.charAt(i + 1) != ' ') {
                        sbtmp.deleteCharAt(i + 1);
                    }
                }
    
            }
        }
        for (int i = 1; i < sbtmp.length() - 1; ++i) {
            if (sbtmp.charAt(i) == ',' && (sbtmp.charAt(i - 1) == ',' || sbtmp.charAt(i + 1) == ',')) {
                sbtmp.deleteCharAt(i);
                --i;
            }
            // 去中间单个字
            else if (sbtmp.charAt(i - 1) == ',' && sbtmp.charAt(i + 1) == ',') {
                sbtmp.deleteCharAt(i);
                sbtmp.deleteCharAt(i);
                --i;
            }
            // 去首个单个字
            else if (sbtmp.charAt(i) == ',' && i == 1) {
                sbtmp.deleteCharAt(i - 1);
                sbtmp.deleteCharAt(i - 1);
                --i;
            }
        }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    3.1.2 提取关键词

    分词并不能很好的将常用的短语提取出来,如词语“用户画像”,使用分词工具更倾向于将其分成“用户”和“画像”,而失去了词语本身的含义。NLPIR还提供了提取一段话的关键词的功能,我们可以使用它:

    int numofIm = 1000;
    String nativeByte = CLibrary.Instance.NLPIR_GetKeyWords(sInput, numofIm, false);    
     
    • 1
    • 2
    • 1
    • 2

    经过分词后,平均每位用户搜索词列所得到的词量在600个左右,这里我们设置提取1000个关键词,但实际上一个用户的关键词提取的数量在60~200左右。由于关键词的很强的特征性,并且提取出的数量又少,若后续我们直接使用如词语的词频作为用户的特征属性进行分类的话,很可能各个用户特征属性有巨大的差异,即用户之间拥有的相同关键词过少。

    3.1.3 混合提取

    在用户搜索词列分词基础上,在增加N次对其进行M个关键词提取的结果。

    3.2 “结巴”分词

    jieba,即“结巴”中文分词,一个优秀的开源的分词工具,一直致力于做最好的 Python 中文分词组件。我们直接使用它对用户搜索词列进行1000个关键词的提取,所能提取到的关键词比NLPIR数量有所提高。显然,关键词提取的数量增加,每个关键词的代表性就有所减弱。但在后续的分类实验中证明了,使用该分词方案,对比上节的各个分词方案,在模型相同的情况下,会有2%~5%的准确率的提升。 
    关键词抽取可基于以下两种算法,后续实验实践证明基于 TF-IDF 算法的关键词的抽取,在该数据集和我们后续所选择的模型中会得到更好的效果。

    3.2.1 基于 TF-IDF 算法的关键词抽取

    import jieba.analyse
    jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())
     
    • 1
    • 2
    • 1
    • 2
    1. sentence 为待提取的文本
    2. topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20
    3. withWeight 为是否一并返回关键词权重值,默认值为 False
    4. allowPOS 仅包括指定词性的词,默认值为空,即不筛选
    5. jieba.analyse.TFIDF(idf_path=None) 新建 TFIDF 实例,idf_path 为 IDF 频率文件

    代码示例 (关键词提取)

    import sys
    sys.path.append('../')
    
    import jieba
    import jieba.analyse
    from optparse import OptionParser
    
    USAGE = "usage:    python extract_tags.py [file name] -k [top k]"
    
    parser = OptionParser(USAGE)
    parser.add_option("-k", dest="topK")
    opt, args = parser.parse_args()
    
    
    if len(args) < 1:
        print(USAGE)
        sys.exit(1)
    
    file_name = args[0]
    
    if opt.topK is None:
        topK = 10
    else:
        topK = int(opt.topK)
    
    content = open(file_name, 'rb').read()
    
    tags = jieba.analyse.extract_tags(content, topK=topK)
    
    print(",".join(tags))
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    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.1 TF-IDF

    • TF(Term Frequency) 词频
    • DF (Document Frequency) 词语出现的文档数目
    • N 总共的文档数目
    • IDF (Invert Document Frequency) 逆文档频率:
    • IDF (Invert Document Frequency) 逆文档频率:

    IDF反映了一个特征词在整个文档集合中的情况,出现的愈多IDF值越低,这个词区分不同文档的能力越差。 
    示例代码:

    import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
    
    val sentenceData = spark.createDataFrame(Seq(
      (0, "Hi I heard about Spark"),
      (0, "I wish Java could use case classes"),
      (1, "Logistic regression models are neat")
    )).toDF("label", "sentence")
    
    val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
    val wordsData = tokenizer.transform(sentenceData)
    val hashingTF = new HashingTF().setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(20)
    val featurizedData = hashingTF.transform(wordsData)
    
    val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
    val idfModel = idf.fit(featurizedData)
    val rescaledData = 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]
    */
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    值得一提的是,Spark所提供的TF并不是数组,而是一个使用 MurmurHash 3 函数的哈希表。其默认向量维度为 2^18=262,144。我们运行上节示例代码可以发现,我们将哈希表大小设置成了20,第二条sentence:”I wish Java could use case classes”有7个不同的单词,经过hash函数却被映射成了只有5个属性为非零值,即有2个位置放了2个不同的单词。这具有很大的随机性,将两个无关词义的词语,甚至词义相反的词语,如“男”与“女”,映射到哈希表的同一位置,作为相同的用户属性来处理。

    4.2 CountVectorizer

    为了解决上节所提到的HashingTF哈希函数映射后导致词语重叠问题,我们使用了Spark的CountVectorizer。我们会先想CountVectorizer传入一个互斥的字符串数组,文本经过CountVectorizer转换后,会对该数组中所有的词语进行与属性的一一对应。 
    我们对互斥的字符串数组进行的优化,过滤掉了词频为1的词语,将CountVectorizer的维度减少到原来的50%,极大的降低了后续训练模型时所需的内存,而且除去的数据噪音,增加了预测的准确度:

    val diffTrain = Triandata.map { line =>
          val temp = line.split("\t")
          if (temp.length == 5) temp(4) else ""
        }
    val diffTest = Testdata.map { line =>
          val temp = line.split("\t")
          if (temp.length == 5) temp(1) else ""
        }
    val diffAll = diffTrain.union(diffTest).flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _).collect.filter(line => line._1 != "" && line._2 > 14).map(line => line._1)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    val cvm = new CountVectorizerModel(diffAll).setInputCol(tokenizer.getOutputCol).setOutputCol("features")  
     
    • 1
    • 1

    4.3 StopWordsRemover

    这里写图片描述 
    在上一章中,我们提到了分词时,根据分词结果所带的词性,对其进行去停用词。而后,我们发现使用”结巴”分词进行TF-IDF算法对用户搜索词列进行1000个关键词的提取对于后续的分类模型效果会更好。但是,我们在“结巴”关键词提取的结果却发现了类似于“什么”“即使”等不具有代表性的词语。于是我们1119个停用词,使用Spark的StopWordsRemover,对分词结果进行去停用词:

    val Stopdata = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/stop",128).collect()
     
    • 1
    • 1
    val remover = new StopWordsRemover().setInputCol("words").setOutputCol("filtered").setStopWords(Stopdata)
     
    • 1
    • 1

    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]。 
     
     是某个特征向量所有元素的最大最小值 
    max,min是用户可以重新自定义的范围,默认为【0,1】,由所有特征共享

    在后续,当我们对特征矩阵进行聚类后,得到的特征值可能为负值,可是很多分类器模型需要特征值为非负值。使用以上方法也可以解决这个问题。

    4.5 同义词替换

    这里写图片描述 
    设想当一个用户的搜索词列的分词结果中出现了一些意思相近的词语,如“恋爱”与“爱情”、“菠萝”与“凤梨”。而我们的模型将其辨别为不同的特征属性,这无疑大量的增加了特征向量的维度和平分了同一意思的词语具有的代表性。 
    为了解决上述问题,我们搜集了近4万条同义词词典,将意思相近的词语由1个词语来替换掉。该优化帮助原本的特征向量减少了3万以上的维度,降低了后续训练模型时所需的内存,而且凝聚了属性的代表性,增加了预测的准确度:

        val sqlContext = new org.apache.spark.sql.SQLContext(sc)
        import sqlContext.implicits._
        val train = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/JBtrain", 400)
        val test = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/JBtest", 400)
        val same = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/same", 400)
        same.filter { x => !x.contains('=') }.count()
        val sameWord = same.map { line =>
          val valuekey = line.split('=')
          (valuekey(1), valuekey(0))
        }.collect()
        val broadcastVar = sc.broadcast(sameWord)
        val diffTrain = train.map { line =>
          val broad = broadcastVar.value
          val regex = """^\d+$""".r
          val temp = line.split("\t")
          val wordArray = temp(4).split(",")
          var str = ""
          for (word <- wordArray) {
            val keyVal = broad.filter(line => line._1.equals(word))
            if (keyVal.length > 0) {
              val oneKeyVal = keyVal(0)
              str = str + "#" + oneKeyVal._2
            } else if (regex.findFirstMatchIn(word) == None) {
              str = str + "#" + word
            }
          }
          (temp(0), temp(1), temp(2), temp(3), str)
        }
        diffTrain.toDF().coalesce(1).write.csv("hdfs://cdh01:8020//user/data/sogou2/ReplaceJBtrain")
    
        val diffTest = test.map { line =>
          val broad = broadcastVar.value
          val regex = """^\d+$""".r
          val temp = line.split("\t")
          val wordArray = temp(1).split(",")
          var str = ""
          for (word <- wordArray) {
            val keyVal = broad.filter(line => line._1.equals(word))
            if (keyVal.length > 0) {
              val oneKeyVal = keyVal(0)
              str = str + "#" + oneKeyVal._2
            } else if (regex.findFirstMatchIn(word) == None) {
              str = str + "#" + word
            }
          }
          (temp(0), str)
        }
        diffTest.toDF().coalesce(1).write.csv("hdfs://cdh01:8020//user/data/sogou2/ReplaceJBtest")
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    4.6 形近词替换

    这里写图片描述 
    设想当一个用户的搜索词列的分词结果中出现了一些形近的词语,如“iPhone5”与“iPhone6”、“杭州”与“杭州站”。该问题和上节所提出的问题类似,为了解决这个问题,我们先来介绍“编辑距离算法”。 
    编辑距离算法,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如,计算X字符串 与 Y字符串 的 编辑距离。dp[i][j] 为 X串的前i个字符 和 Y串的前j个字符 的编辑距离:

    if(X [i - 1] == Y[j - 1])       
        dp[i][j] = dp[i - 1][j - 1];   //最后字符相同    
    else
    { 
        int t1 = dp[i - 1][j];                             //删除X第i个字符    
        t1 = t1 < dp[i][j - 1] ? t1 : dp[i][j - 1];        //删除Y第j个字符    
        t1 = t1 < dp[i - 1][j - 1] ? t1 : dp[i - 1][j - 1];//最后字符改相同
        dp[i][j] = t1 + 1;
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里我们所使用的优化方案为: 
    这里写图片描述

    • 对整个训练集和测试集的搜索词列做分词后的词频统计表
    • 对每个用户的搜索词列分词后的各个词与词频统计表各词(排除前者自身)进行编辑距离计算。
    • 得到词频统计表中编辑距离与该词编辑距离最小词,在这些词中在选择一个词频最高的词将该词替代。

    4.7 额外增加数据量

    这里写图片描述 
    在大数据时代背景下,只要数据量足够的大,反而我们所选用的不同的算法模型对最终的预测准确率的影响会变小,获取更多数据会使模型更完善更准确。我们这里用不同方案所得到的分词结果,人为的增加训练集的数据。如将10万条记录的训练集进行NLPIR分词得到结果,与进行”结巴”提取关键词得到的结果拼接,就将训练集记录人为的翻倍了。后续的分类实验中证明了,使用该方案,在模型相同的情况下,相比原来会有1%左右的准确率的提升。

    5.聚类与降维

    2009年结束的Nexfix竞赛表明,很多参数团队用到的高等矩阵因子分解对模型提高预测准确略非常有帮助。模型使用矩阵因子分解方法从特征矩阵中抽取一组潜在的属性,并通过这些属性来描述用户。20世纪80年代后期,利用潜在的”语义”属性的思想被成功的应用于信息检索领域。Deerwesteret al. 在1990年提出使用奇异值分解(SVD)方法发现文档中的潜在的属性。[2]而本课题在实验中会使用到LDA方法。

    5.1 LDA

    隐含狄利克雷分配(LDA,Latent Dirichlet Allocation)是一种主题模型(Topic Model,即从所收集的文档中推测主题)。 甚至可以说LDA模型现在已经成为了主题建模中的一个标准,是实践中最成功的主题模型之一。那么何谓“主题”呢?,就是诸如一篇文章、一段话、一个句子所表达的中心思想。不过从统计模型的角度来说, 我们是用一个特定的词频分布来刻画主题的,并认为一篇文章、一段话、一个句子是从一个概率模型中生成的。也就是说 在主题模型中,主题表现为一系列相关的单词,是这些单词的条件概率。形象来说,主题就是一个桶,里面装了出现概率较高的单词(参见下面的图),这些单词与这个主题有很强的相关性。 
    这里写图片描述 
    LDA可以用来识别大规模文档集或语料库中潜藏的主题信息。它采用了词袋的方法,这种方法将每一篇文档视为一个词频向量,从而将文本信息转化为了易于建模的数字信息。但是词袋方法没有考虑词与词之间的顺序,这简化了问题的复杂性,同时也为模型的改进提供了契机。每一篇文档代表了一些主题所构成的一个概率分布,而每一个主题又代表了很多单词所构成的一个概率分布。 
    LDA可以被认为是如下的一个聚类过程:

    • 各个主题(Topics)对应于各类的“质心”,每一篇文档被视为数据集中的一个样本。
    • 主题和文档都被认为存在一个向量空间中,这个向量空间中的每个特征向量都是词频(词袋模型)
    • 与采用传统聚类方法中采用距离公式来衡量不同的是,LDA使用一个基于统计模型的方程,而这个统计模型揭示出这些文档都是怎么产生的。

    5.1.1 模型训练

    Spark API 参数介绍:

    • K:主题数量(或者说聚簇中心数量)
    • maxIterations:EM算法的最大迭代次数,设置足够大的迭代次数非常重要,前期的迭代返回一些无用的(极其相似的)话题,但是继续迭代多次后结果明显改善。我们注意到这对EM算法尤其有效。,至少需要设置20次的迭代,50-100次是更合理的设置,取决于数据集。
    • docConcentration(Dirichlet分布的参数α):文档在主题上分布的先验参数(超参数α)。当前必须大于1,值越大,推断出的分布越平滑。默认为-1,自动设置。
    • topicConcentration(Dirichlet分布的参数β):主题在单词上的先验分布参数。当前必须大于1,值越大,推断出的分布越平滑。默认为-1,自动设置。
    • checkpointInterval:检查点间隔。maxIterations很大的时候,检查点可以帮助减少shuffle文件大小并且可以帮助故障恢复。
        val lda=new LDA()
                    .setK(20)
                    .setOptimizer("online")
                    .setCheckpointInterval(10)
                    .setMaxIter(100)
        val model=lda.fit(dataset_lpa)   
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5.1.2 模型评价

    生成的model不仅存储了推断的主题,还包括模型的评价方法。模型的评价指标:logLikelihood,logPerplexity。logLikelihood越大越好,logPerplexity越小越好

        val ll = model.logLikelihood(dataset_lpa)
        val lp = model.logPerplexity(dataset_lpa)
     
    • 1
    • 2
    • 1
    • 2

    用评价方法,在online 方法下,对setMaxIter进行调参:

    for(i<-Array(5,10,20,40,60,120,200,500)){
        val lda=new LDA()
                    .setK(3)
                    .setTopicConcentration(3)
                    .setDocConcentration(3)
                    .setOptimizer("online")
                    .setCheckpointInterval(10)
                    .setMaxIter(i)
        val model=lda.fit(dataset_lpa) 
    
        val ll = model.logLikelihood(dataset_lpa) 
        val lp = model.logPerplexity(dataset_lpa)
    
        println(s"$i $ll")
        println(s"$i $lp")
     }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看到,logPerplexity在减小,LogLikelihood在增加,最大迭代次数需要设置50次以上,才能收敛: 
    这里写图片描述 
    这里写图片描述

    5.1.3 对语料的主题进行聚类

        val topicsProb=model.transform(dataset_lpa)
        topicsProb.select("label", "topicDistribution")show(false)
        /** 
            +-----+--------------------------------------------------------------+
            |label|topicDistribution                                             |
            +-----+--------------------------------------------------------------+
            |0.0  |[0.523730754859981,0.006564444943344147,0.46970480019667477]  |
            |1.0  |[0.7825074858166653,0.011001204994496623,0.206491309188838]   |
            |2.0  |[0.2085069748527087,0.005698459472719417,0.785794565674572]   |
            ...
    
        */
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    label是文档序号,文档中各主题的权重,我们可以将该DataFrame带入后续的分类器中,进行训练。

    5.1.4 其他聚类与降维

    Spark在基于RDD的MLlib中还提供了SVD、PCA的降维方法,而基于DataFrame的聚类方法还包括k-means、Bisecting k-means和Gaussian Mixture,其中Gaussian Mixture提供的API类似与LDA,可以直接为我们返回文档中各主题的权重,以便于后续的分类。但是由于LDA在主题聚类上的典型性,我们的课题实验只试验了LDA的方案。

    6. 分类方案

    6.1 Cosine相似度

    假设现在我们有一个测试集特征向量A和一个训练集的特征向量B:

    A:[1, 2, 2, 1, 1, 1, 0] 
    B:[1, 2, 2, 1, 1, 2, 1]

    到这里,问题就变成了如何计算这两个向量的相似程度。我们可以把它们想象成空间中的两条线段,都是从原点([0, 0, …])出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。

    这里写图片描述

    以二维空间为例,上图的a和b是两个向量,我们要计算它们的夹角θ。余弦定理告诉我们,可以用下面的公式求得: 

    拓展到n维向量,假定A和B是两个n维向量,A是 [A1, A2, …, An] ,B是 [B1, B2, …, Bn] ,则A与B的夹角θ的余弦等于: 

    使用这个公式,我们就可以得到,特征向量A与特征向量B的夹角的余弦: 

    余弦值越接近1,就表明夹角越接近0度,也就是两个向量越相似,这就叫”余弦相似度” 
    我们这个方案,计算出一条测试集的特征向量与训练集各个特征向量的余弦相似度,将该条测试集的类别标记为与其余弦相似度最大的训练集特征向量所对应的类别。

    6.2 One-vs-Rest

    One-vs-Rest将只能用于二分问题的分类(如Logistic回归、SVM)方法扩展到多类。这种方法基本思想为:

    训练时依次把某个类别的样本归为一类,其他剩余的样本归为另一类,这样k个类别的样本就构造出了k个binary分类器。分类时将未知样本分类为具有最大分类函数值的那类。 
    假如我有四类要划分(也就是4个Label),他们是A、B、C、D。于是在抽取训练集的时候,分别抽取 
    (1)A所对应的向量作为正集,B,C,D所对应的向量作为负集 
    (2)B所对应的向量作为正集,A,C,D所对应的向量作为负集; 
    (3)C所对应的向量作为正集,A,B,D所对应的向量作为负集; 
    (4)D所对应的向量作为正集,A,B,C所对应的向量作为负集; 
    使用这四个训练集分别进行训练,然后的得到四个训练结果文件。在测试的时候,把对应的测试向量分别利用这四个训练结果文件进行测试。最后每个测试都有一个结果f1(x),f2(x),f3(x),f4(x)。于是最终的结果便是这四个值中最大的一个作为分类结果。

    代码实例:

    //定义一个binary分类器,如:LogisticRegression 
    LogisticRegression lr=new LogisticRegression()
                    .setMaxIter(10)
                    .setRegParam(0.3)
                    .setElasticNetParam(0.2)                
                    .setThreshold(0.5);
    //建立一对多多分类器model                
    OneVsRestModel model=new OneVsRest()
                    .setClassifier(lr)//将binary分类器用这种办法加入
                    .fit(training);
    //利用多分类器model预测
    Dataset<Row>predictions=model.transform(test);  
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    很遗憾的是,目前Spark基于DataFrame的MLlib binary分类器中并没有实现SVM,而基于RDD的MLlib有实现SVM,却没有实现One-vs-Rest。

    6.3 朴素贝叶斯

    朴素贝叶斯分类是一种思想比较简单的分类算法,朴素贝叶斯的思想基础是这样的:对于给出的待分类项,求解在此项出现的条件下各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。 
    朴素贝叶斯分类的正式定义如下:

    1、设为一个待分类测试特征向量,而每个为属性的值。

    2、有类别集合 

    3、计算 

    4、将该待分类的用户类别标记为

    那么现在的关键就是如何计算第3步中的各个条件概率。我们假设各个特征属性是条件独立的,则根据贝叶斯定理有如下推导:

    因为分母对于所有类别为常数,因为我们只要将分子最大化皆可。又因为各特征属性是条件独立的,所以有:

    而等式右边的各项都可以有训练集求出。以此类推,便可以计算出计算第3步。 
    Spark Mllib实现朴素贝叶斯分类器,Smoothing为平滑系数:

    val lr = new NaiveBayes().setSmoothing(0.1).setFeaturesCol("features")
     
    • 1
    • 1

    6.4 前馈神经网络

    Spark MLlib中实现了MultilayerPerceptronClassifier(MLPC),这是一个基于前馈神经网络的分类器,它是一种在输入层与输出层之间含有一层或多层隐含结点的具有正向传播机制的神经网络模型。

    这里写图片描述

    中间的节点使用sigmoid (logistic)函数,输出层的节点使用softmax函数。输出层的节点的数目表示分类器有几类。MLPC学习过程中使用BP算法,优化问题抽象成logistic loss function并使用L-BFGS进行优化。 
    下面我们来介绍下MultilayerPerceptronClassifier所设计到的参数:

    val lr = new MultilayerPerceptronClassifier().setMaxIter(51).setLayers(Array[Int](vector_len, 6, 5, classfiy_num)).setSeed(1234L).setFeaturesCol("features").setLabelCol("label").setPredictionCol("prediction")
     
    • 1
    • 1
    • layers:各层的节点数,第1层的个数必须为特征向量的维度数,最后1层的个数必须为类别数,中间为各隐藏层的节点数,可以任意设置。对于隐藏成节点数的优化,我们做了一下的方案:由于我们的年龄、性别、学历的类别数分别是6、2、6。假设我们在预测年龄时,我们可以将隐藏的第1层节点数设为6 * 2 * 6 = 72,即根据先将其分为3个label都互斥的类别,。然后,将隐藏的第2层节点数设为6 * 2 = 12,即根据先将其分为年龄和性别3个label都互斥的类别。最后,再分到我们想预测的年龄的6个类别上。
    • maxIter:最大迭代次数。进行参数调优时,主要是在优化这个参数。
    • Seed:随机种子
    • tol:(代码中未设置)允许误差。一般取0.001~0.00001,当迭代结果的误差小于该值时,结束迭代计算,给出结果。
    • solver:(代码中未设置)优化器。有两种算法可供选择: l-bfgs和gd。默认为 l-bfgs算法,因为gd算法比l-bfgs算法需要多得多的迭代次数,即使在提高学习步长和提高允许误差tol的情况下,还是慢很多:

      .stepSize=0.03,tol=0.0001 
      l-bfgs:上很快能收敛,大约20次,训练速度也更快 
      maxIter = 5 accuracy = 0.35 training time = 267ms 
      maxIter = 10 accuracy = 0.74 training time = 429ms 
      maxIter = 20 accuracy = 0.91 training time = 657ms 
      maxIter = 50 accuracy = 0.92 training time = 941ms 
      maxIter = 100 accuracy = 0.92 training time = 914ms 
      maxIter = 500 accuracy = 0.92 training time = 1052ms

      这里写图片描述

      gd算法:基本上要比l-bfgs算法慢10以上 
      stepsize=0.2,tol=0.001 
      maxIter = 100 accuracy = 0.55 training time = 4209ms 
      maxIter = 500 accuracy = 0.92 training time = 11216ms 
      maxIter = 1000 accuracy = 0.92 training time = 14540ms 
      maxIter = 2000 accuracy = 0.92 training time = 14708ms 
      maxIter = 5000 accuracy = 0.92 training time = 14669ms

    • stepSize:学习步长。一般来说学习步长越大,权重变化越大,收敛越快;但训练速率过大,会引起系统的振荡。太高的学习步长,可以减少网络训练的时间,但是容易导致网络的不稳定与训练误差的增加,会引起系统的振荡。

    6.5 预分类迭代预测

    这里写图片描述 
    数据表明,当我们在进行对年龄进行分类时,其实同一年龄段的男性和女性查询语句是存在较大的差异的。我们可以借鉴6.4 前馈神经网络中对layers(各层的节点数)优化的类似的思想: 
    1. 将测试集进行性别男女的预测分类,预测成男性的分为测试集test1,预测成女性的分为测试集test2。 
    2. 将训练集根据性别划分为train1(男性)和train2(女性)。 
    3. 分别用train1训练模型来预测test1的年龄,train2训练模型来预测test2年龄。 
    上诉思想可以实现一种迭代,即继续对年龄的预测结果进行划分来预测学历,再对学历的预测结果进行划分来预测性别,在进行上诉的第2、第3步,如此反复继续。 
    这个方案由于将数据集划分了,每次降低了数据量,这会对准确率有负面的影响,但最终总体预测准确率提高了0.3%左右。

    6.6 Boosting

    Boosting分类器属于集成学习模型,它基本思想是把成百上千个分类准确率较低的树模型组合起来,成为一个准确率很高的模型。这个模型会不断地迭代,每次迭代就生成一颗新的树。

    6.6.1 xgboost

    xgboost 的全称是eXtreme Gradient Boosting。它是Gradient Boosting Machine的一个c++实现,作者为华盛顿大学研究机器学习专家陈天奇 。他在研究中深感自己受制于现有库的计算速度和精度,因此开始着手搭建xgboost项目。xgboost最大的特点在于,它能够自动利用CPU的多线程进行并行,同时在算法上加以改进提高了精度。在Kaggle的希格斯子信号识别竞赛中,它因为出众的效率与较高的预测准确度在比赛论坛中引起了参赛选手的 广泛关注 ,在1700多支队伍的激烈竞争中占有一席之地。 
    xgboost拥有自身的jvm项目包,可以和Spark集成。很遗憾的是,由于时间关系,我们并没有成功尝试这个方案,而是在已有分类的结果上实现了简单的加权投票的策略,最终总体预测准确率提高了0.4%左右。

    7. 参数调优

    7.1 交叉验证法

    Spark Mllib 中实现的是留一法交叉验证法。留一法交叉验证法的思想是:将原来的训练集有N个数据集,将每一个数据集作为测试集,其它N-1个数据集作为训练集。这样得到N个分类器,N个测试结果。用这N个结果的平均值来衡量模型的性能:

    val pipeline = new Pipeline()
      .setStages(Array(tokenizer, hashingTF, lr))
    
    val paramGrid = new ParamGridBuilder()
      .addGrid(hashingTF.numFeatures, Array(10, 100, 1000))
      .addGrid(lr.regParam, Array(0.1, 0.01))
      .build()
    
    val cv = new CrossValidator()
      .setEstimator(pipeline)
      .setEvaluator(new BinaryClassificationEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setNumFolds(2)  
    
    val cvModel = cv.fit(training)
    
    ......
    
    cvModel.transform(test)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    参数介绍:

    • Estimator:即所要进行评估的模型
    • Evaluator:对模型评估器,可以是二分类的BinaryClassificationEvaluator 或是 多分类的 MulticlassClassificationEvaluator
    • EstimatorParamMaps:模型参数表。可以由ParamGridBuilder调用addGrid方法对模型的某个参数设置一组需要验证的值,再调用build()返回。
    • NumFolds:即所要划分的数据集的数量N

    7.2 划分训练集验证法

    划分训练集验证法的思想比较简单,我们将训练集按 m:1 - m 的比例划分成两个部分,第1部分作为新的训练集,第2部分作为验证集:

    val trainValidationSplit = new TrainValidationSplit()
      .setEstimator(lr)
      .setEvaluator(new RegressionEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setTrainRatio(0.8)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5

    我们可以从上述代码看到参数基本上和交叉验证法相同,其中TrainRatio就是我们说的m。 
    划分训练集验证法优点在于所需要的时间较短。当我们在对前馈神经网络参数调优时,因为耗时过长而无法选用交叉验证法,而划分训练集验证法则是一种很好的替代方案。 
    很遗憾的是,Spark Mllib所实现的交叉验证法和划分训练集验证法都没有返回验证所选得的一组最优参数的API,而是将其视为一种模型直接对原始训练集进行训练,最后返回预测结果。而且,划分训练集验证法只对训练集划分一次进行预测,这具有很大的偶然性。由于以上的原因,我们动手自己实现了划分训练集验证法,并每次验证进行了三次的随机划分和训练,以其平均值作为验证的结果,最后按准确率对参数组降序排序。:

       val evaluations =
          for (
            smooth <- 1 to 100
          ) yield {
            val lr = new NaiveBayes().setSmoothing(smooth.toDouble / 100.0).setFeaturesCol("features")
            val pipeline = new Pipeline().setStages(Array(lr))
            var sumA = 0.0
            for (cnt <- 1 to 3) {
              val Array(trainData, testData) = dataIDF.randomSplit(Array(0.7, 0.3))
              trainData.cache()
              testData.cache()
              val model = pipeline.fit(trainData)
              val predictions = model.transform(testData)
              val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction").setMetricName("accuracy")
              val accuracy = evaluator.evaluate(predictions)
              sumA = sumA + accuracy
            }
            val allAccuracy = sumA / 3.0
            println(((smooth), allAccuracy))
            ((smooth), allAccuracy)
          }
        evaluations.sortBy(_._2).reverse.foreach(println)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    使用上述代码对贝叶斯分类器关于各个label预测的参数(平滑度)调优,总体平均准确率相比原来有6%左右的提升。

    8. 回顾与展望

    8.1 总结

    经过我们对各个分词、特征提取与转换、聚类、分类以及参数调优的方案的尝试,最终我们选择了:

    • 分词:NLPIR分词、结巴TF-IDF与结巴TextRank叠加,将训练集数据量变为原来的3倍。
    • 特征提取与转换:选择了HashingTF,维度设置为10万。
    • 聚类:弃用。
    • 分类:使用前馈神经网络,对每个Label预测,设置参数都为:
    MultilayerPerceptronClassifier().setMaxIter(32).setLayers(Array[Int](vector_len, 6, 5, classfiy_num))
    
     
    • 1
    • 2
    • 1
    • 2
    • 参数调优:将HashingTF维度降低为1万,使用划分训练集验证法进行调优。

    该方案所能达到的平均准确率为69.652%,再对其他方案与该方案进行简单的加权投票,平均准确率可达70%左右。

    8.2 不足与展望

    从上节可以发现,很遗憾的我们在最终的方案中做了很多原本优化上的妥协:

    • 分词:从最终分词结果的数据上表明,分词结果还存在非常大可以改进的地方,还存在许多代表性极弱的词语。
    • 特征提取与转换:由于模型的选择,试验证明使用IDF和权值规范化,并不能得到预期的效果。反而,只使用CountVectorizer会得到较好的效果。而在选用前馈神经网络分类器后,Spark机器无法支撑CountVectorizer如此大的维度(按结巴TF-IDF分词结果,使用CountVectorizer会得到维度为60多万的特征向量,而去词频为1的词语后得到的特征向量维度为30多万),因而我们退而求其次选用了HashingTF。
    • 聚类:实验中的Spark集群使用LDA,所能承受特征维度在30万以下,影响了LDA的效果。而训练一次(迭代100次)所花费的时间,我们实在无法承受,再而我们对参数的设置(如主题个数、迭代次数)并没太大的经验。在多次试验,得到后续的分类效果并不良好的情况下,我们放弃了这个方案。
    • 分类与参数调优:当我们弃用朴素贝叶斯分类器,转而试用并未进行参数调优的前馈神经网络分类器后,惊喜的发现后者比前者参数调优过的平均预测准确率还要高出4%左右。但很遗憾的是,就算我们将特征向量维度降低至10万,对前馈神经网络分类器的参数调优所耗费的时间还是远远的超出我们所能承受的。于是,我们尝试将维度降低至1万,调优得到参数组后,再带回到10万维度的特征向量;尝试减少数据量进行参数调优。而结果证明,这两种方案的调优都不能与使用原始的特征向量调优效果一致。
    • xgboost:由于时间关系,我们并没有成功尝试这个方案,而是在已有分类的结果上实现了简单的加权投票的策略。

    由于我们的经验不足,再加上实验室的项目任务、在校课程的学习任务以及后期转而对Spark Core源码的研究,使得原本可以做的优化并没有精力去很好的实现。希望在后续能将其完善的更好。

    8.3 感谢

    感谢实验室所提供的服务器集群环境,感谢同组同学在一起交流讨论中激发出灵感。非常感谢这次课题实验给我带来的学习机会,让我从头到尾自主的完成了一次数据处理、分析的过程,也深深的感受到了Spark的魅力和大数据处理的重要性,也坚定了我从事Spark大数据处理与分析研究的决心。

    9. 参考文献

    【1】Mihalcea R, Tarau P. TextRank: Bringing order into texts[C]. Association for Computational Linguistics, 2004. 
    【2】Dietmar J, Markus Z. Recommender systems: An introduction[M]. Cambridge University Press, 2010: 35.

    展开全文
  • 近期参加了CCF举办的“大数据精准营销中搜狗用户画像挖掘”竞赛,就那它来写了。本博文会在这几周不断的完善更新ing 1. 选题背景与意义 1.1 用户画像与精准营销   “用户画像”是近几年诞生的名词。很多...

    转载请注明:转载 from http://blog.csdn.net/u011239443/article/details/53735609 
    近期参加了CCF举办的“大数据精准营销中搜狗用户画像挖掘”竞赛,就那它来写了。本博文会在这几周不断的完善更新ing

    1. 选题背景与意义

    1.1 用户画像与精准营销

    这里写图片描述 
    “用户画像”是近几年诞生的名词。很多营销项目或很多广告主,在打算投放广告前,都要求媒体提供其用户画像。在以前,大多媒体会针对自身用户做一个分类,但是有了大数据后,企业及消费者行为带来一系列改变与重塑,通过用户画像可以更加拟人化的描述用户特点。 
    用户画像,即用户信息标签化,就是企业通过收集与分析消费者社会属性、生活习惯、消费行为等主要信息的数据之后,完美地抽象出一个用户的商业全貌,可以看作是企业应用大数据技术的基本方式。用户画像为企业提供了足够的信息基础,能够帮助企业快速找到精准用户群体以及用户需求等更为广泛的反馈信息。 
    消费方式的改变促使用户迫切希望尽快获取自己想要了解的信息,所以说,基于用户画像上的精准营销不管对企业还是对用户来说,都是有需求的,这会给双方交易带来极大便捷,也为双方平等沟通搭建了一个畅通平台。

    1.2 搜索引擎下用户画像的挑战

    这里写图片描述 
    在搜索引擎下,由于搜索引擎本身使用方式的特殊性、用户的流动性、查询的实时性等,带来了与企业传统的对用户信息进行收集与分析有着巨大的不同、更加艰巨的挑战。 
    例如,我们实时获取到的是用户的查询语句,而由于用户的流动性,并不能直接获取到如年龄、性别、学历等用户的标签信息。这么一来,也就无法根据用户属性对用户进行分群处理,而后再通过推荐系统进行产品上的优化

    1.3 本文内容概要

    本文内容概要如下:

    • 第1章:简介用户画像与搜索引擎下用户画像的精准营销的挑战。
    • 第2章:说明实验集群、数据与课题研究目标。
    • 第3章:介绍使用分词工具对用户的搜索词列进行分词,以及相关的优化方案。
    • 第4章:介绍在分词的基础上,对文本进行特征的抽取与转换,以及相关的优化方案。
    • 第5章:介绍在原始特征向量上,进行聚类与降维。
    • 第6章:介绍实验中试验过各分类模型
    • 第7章:介绍模型参数调优
    • 第8章:总结本课题研究中不足与展望后续的优化方案
    • 第9章:参考文献

    2. 课题实验准备

    2.1 Spark集群

    节点 备注
    cdh01 8核,32G内存,角色:Spark Master,HDFS NameNode,Spark Worker,HDFS DataNode
    cdh02 8核,12G内存,角色:Spark Worker,HDFS DataNode
    cdh03 8核,12G内存,角色:Spark Worker,HDFS DataNode
    cdh04 8核,12G内存,角色:Spark Worker,HDFS DataNode

    2.2 数据集

    数据文件 备注
    Train.csv 带标注的训练集
    Test.csv 测试集

    2.3 数据介绍

    本数据来源于搜狗搜索数据,ID经过加密,训练集中人口属性数据存在部分未知的情况(需要解决方案能够考虑数据缺失对算法性能的影响)。数据所有字段如下表所示:

    字段 说明
    ID 加密后的ID
    age 0:未知年龄; 1:0-18岁; 2:19-23岁; 3:24-30岁; 4:31-40岁; 5:41-50岁; 6: 51-999岁
    Gender 0:未知1:男性2:女性
    Education 0:未知学历; 1:博士; 2:硕士; 3:大学生; 4:高中; 5:初中; 6:小学
    Query List 搜索词列表

    2.4 数据示例

    对于train.csv中的数据记录:

    00627779E16E7C09B975B2CE13C088CB 4 2 0 钢琴曲欣赏100首 一个月的宝宝眼睫毛那么是黄色 宝宝右眼有眼屎 小儿抽搐怎么办 剖腹产后刀口上有线头 属羊和属鸡的配吗

    2.5 课题任务描述

    根据提供的用户历史一个月的查询词与用户的人口属性标签(包括性别、年龄、学历)做为训练数据,通过机器学习、数据挖掘技术构建分类算法来对新增用户的人口属性进行判定。

    3. 查询词分词

    3.1 NLPIR

    这里写图片描述 
    NLPIR汉语分词系统(又名ICTCLAS2013),主要功能包括中文分词;词性标注;命名实体识别;用户词典功能;支持GBK编码、UTF8编码、BIG5编码。新增微博分词、新词发现与关键词提取;张华平博士先后倾力打造十余年,内核升级10次。 
    全球用户突破20万,先后获得了2010年钱伟长中文信息处理科学技术奖一等奖,2003年国际SIGHAN分词大赛综合第一名,2002年国内973评测综合第一名。 
    我们传入每个用户的搜索词列,表经过NLPIR分词工具得到的分词。之后,我们做个进一步的优化策略:

    3.1.1 去停用词

    我们根据分词后词语所带的词性,对一些特征代表性不够强的词语进行过滤:

        for (int i = 0; i < sbtmp.length(); ++i) {
            char cc = sbtmp.charAt(i);
            if (cc == ' ') {
                sbtmp.deleteCharAt(i);
                --i;
            } else if (cc == '/') {
    
                // 去词条件
                Boolean isdel =
                        // 1. 去标点
                        (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'w')
                                // 2. 疑问词
                                || (i + 2 < sbtmp.length() && sbtmp.charAt(i + 1) == 'r'
                                        && sbtmp.charAt(i + 2) == 'y')
                                // 3. 数字
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'm')
                                // 4. 连词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'c')
                                // 5. 副词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'd')
                                // 6. 叹词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'e')
                                // 7. 拟声词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'o')
                                // 8. 介词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'p')
                                // 9. 量词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'q')
                                // 10. 助词
                                || (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) == 'u')
                                // 11. 纯动词
                                || (i + 2 < sbtmp.length() && sbtmp.charAt(i + 1) == 'v'
                                        && sbtmp.charAt(i + 2) == ' ');
    
                // 去词
                if (sbtmp.charAt(i + 1) != 'n' && sbtmp.charAt(i + 1) != 'i' && sbtmp.charAt(i + 1) != 'j'
                        && sbtmp.charAt(i + 1) != 'h'
                        && !(i + 2 < sbtmp.length() && sbtmp.charAt(i + 2) == 'n')) {
                    while (i + 1 < sbtmp.length() && sbtmp.charAt(i + 1) != ' ') {
                        sbtmp.deleteCharAt(i + 1);
                    }
                    while (i >= 0 && sbtmp.charAt(i) != ',') {
                        sbtmp.deleteCharAt(i);
                        --i;
                    }
                }
                // 若无需去词,把‘/’转为‘,’,并去除随后的词性标志
                else {
                    sbtmp.setCharAt(i, ',');
                    while (sbtmp.charAt(i + 1) != ' ') {
                        sbtmp.deleteCharAt(i + 1);
                    }
                }
    
            }
        }
        for (int i = 1; i < sbtmp.length() - 1; ++i) {
            if (sbtmp.charAt(i) == ',' && (sbtmp.charAt(i - 1) == ',' || sbtmp.charAt(i + 1) == ',')) {
                sbtmp.deleteCharAt(i);
                --i;
            }
            // 去中间单个字
            else if (sbtmp.charAt(i - 1) == ',' && sbtmp.charAt(i + 1) == ',') {
                sbtmp.deleteCharAt(i);
                sbtmp.deleteCharAt(i);
                --i;
            }
            // 去首个单个字
            else if (sbtmp.charAt(i) == ',' && i == 1) {
                sbtmp.deleteCharAt(i - 1);
                sbtmp.deleteCharAt(i - 1);
                --i;
            }
        }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    3.1.2 提取关键词

    分词并不能很好的将常用的短语提取出来,如词语“用户画像”,使用分词工具更倾向于将其分成“用户”和“画像”,而失去了词语本身的含义。NLPIR还提供了提取一段话的关键词的功能,我们可以使用它:

    int numofIm = 1000;
    String nativeByte = CLibrary.Instance.NLPIR_GetKeyWords(sInput, numofIm, false);    
     
    • 1
    • 2
    • 1
    • 2

    经过分词后,平均每位用户搜索词列所得到的词量在600个左右,这里我们设置提取1000个关键词,但实际上一个用户的关键词提取的数量在60~200左右。由于关键词的很强的特征性,并且提取出的数量又少,若后续我们直接使用如词语的词频作为用户的特征属性进行分类的话,很可能各个用户特征属性有巨大的差异,即用户之间拥有的相同关键词过少。

    3.1.3 混合提取

    在用户搜索词列分词基础上,在增加N次对其进行M个关键词提取的结果。

    3.2 “结巴”分词

    jieba,即“结巴”中文分词,一个优秀的开源的分词工具,一直致力于做最好的 Python 中文分词组件。我们直接使用它对用户搜索词列进行1000个关键词的提取,所能提取到的关键词比NLPIR数量有所提高。显然,关键词提取的数量增加,每个关键词的代表性就有所减弱。但在后续的分类实验中证明了,使用该分词方案,对比上节的各个分词方案,在模型相同的情况下,会有2%~5%的准确率的提升。 
    关键词抽取可基于以下两种算法,后续实验实践证明基于 TF-IDF 算法的关键词的抽取,在该数据集和我们后续所选择的模型中会得到更好的效果。

    3.2.1 基于 TF-IDF 算法的关键词抽取

    import jieba.analyse
    jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())
     
    • 1
    • 2
    • 1
    • 2
    1. sentence 为待提取的文本
    2. topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20
    3. withWeight 为是否一并返回关键词权重值,默认值为 False
    4. allowPOS 仅包括指定词性的词,默认值为空,即不筛选
    5. jieba.analyse.TFIDF(idf_path=None) 新建 TFIDF 实例,idf_path 为 IDF 频率文件

    代码示例 (关键词提取)

    import sys
    sys.path.append('../')
    
    import jieba
    import jieba.analyse
    from optparse import OptionParser
    
    USAGE = "usage:    python extract_tags.py [file name] -k [top k]"
    
    parser = OptionParser(USAGE)
    parser.add_option("-k", dest="topK")
    opt, args = parser.parse_args()
    
    
    if len(args) < 1:
        print(USAGE)
        sys.exit(1)
    
    file_name = args[0]
    
    if opt.topK is None:
        topK = 10
    else:
        topK = int(opt.topK)
    
    content = open(file_name, 'rb').read()
    
    tags = jieba.analyse.extract_tags(content, topK=topK)
    
    print(",".join(tags))
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    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.1 TF-IDF

    • TF(Term Frequency) 词频
    • DF (Document Frequency) 词语出现的文档数目
    • N 总共的文档数目
    • IDF (Invert Document Frequency) 逆文档频率:
    • IDF (Invert Document Frequency) 逆文档频率:

    IDF反映了一个特征词在整个文档集合中的情况,出现的愈多IDF值越低,这个词区分不同文档的能力越差。 
    示例代码:

    import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
    
    val sentenceData = spark.createDataFrame(Seq(
      (0, "Hi I heard about Spark"),
      (0, "I wish Java could use case classes"),
      (1, "Logistic regression models are neat")
    )).toDF("label", "sentence")
    
    val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
    val wordsData = tokenizer.transform(sentenceData)
    val hashingTF = new HashingTF().setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(20)
    val featurizedData = hashingTF.transform(wordsData)
    
    val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
    val idfModel = idf.fit(featurizedData)
    val rescaledData = 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]
    */
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    值得一提的是,Spark所提供的TF并不是数组,而是一个使用 MurmurHash 3 函数的哈希表。其默认向量维度为 2^18=262,144。我们运行上节示例代码可以发现,我们将哈希表大小设置成了20,第二条sentence:”I wish Java could use case classes”有7个不同的单词,经过hash函数却被映射成了只有5个属性为非零值,即有2个位置放了2个不同的单词。这具有很大的随机性,将两个无关词义的词语,甚至词义相反的词语,如“男”与“女”,映射到哈希表的同一位置,作为相同的用户属性来处理。

    4.2 CountVectorizer

    为了解决上节所提到的HashingTF哈希函数映射后导致词语重叠问题,我们使用了Spark的CountVectorizer。我们会先想CountVectorizer传入一个互斥的字符串数组,文本经过CountVectorizer转换后,会对该数组中所有的词语进行与属性的一一对应。 
    我们对互斥的字符串数组进行的优化,过滤掉了词频为1的词语,将CountVectorizer的维度减少到原来的50%,极大的降低了后续训练模型时所需的内存,而且除去的数据噪音,增加了预测的准确度:

    val diffTrain = Triandata.map { line =>
          val temp = line.split("\t")
          if (temp.length == 5) temp(4) else ""
        }
    val diffTest = Testdata.map { line =>
          val temp = line.split("\t")
          if (temp.length == 5) temp(1) else ""
        }
    val diffAll = diffTrain.union(diffTest).flatMap(_.split(",")).map((_, 1)).reduceByKey(_ + _).collect.filter(line => line._1 != "" && line._2 > 14).map(line => line._1)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    val cvm = new CountVectorizerModel(diffAll).setInputCol(tokenizer.getOutputCol).setOutputCol("features")  
     
    • 1
    • 1

    4.3 StopWordsRemover

    这里写图片描述 
    在上一章中,我们提到了分词时,根据分词结果所带的词性,对其进行去停用词。而后,我们发现使用”结巴”分词进行TF-IDF算法对用户搜索词列进行1000个关键词的提取对于后续的分类模型效果会更好。但是,我们在“结巴”关键词提取的结果却发现了类似于“什么”“即使”等不具有代表性的词语。于是我们1119个停用词,使用Spark的StopWordsRemover,对分词结果进行去停用词:

    val Stopdata = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/stop",128).collect()
     
    • 1
    • 1
    val remover = new StopWordsRemover().setInputCol("words").setOutputCol("filtered").setStopWords(Stopdata)
     
    • 1
    • 1

    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]。 
     
     是某个特征向量所有元素的最大最小值 
    max,min是用户可以重新自定义的范围,默认为【0,1】,由所有特征共享

    在后续,当我们对特征矩阵进行聚类后,得到的特征值可能为负值,可是很多分类器模型需要特征值为非负值。使用以上方法也可以解决这个问题。

    4.5 同义词替换

    这里写图片描述 
    设想当一个用户的搜索词列的分词结果中出现了一些意思相近的词语,如“恋爱”与“爱情”、“菠萝”与“凤梨”。而我们的模型将其辨别为不同的特征属性,这无疑大量的增加了特征向量的维度和平分了同一意思的词语具有的代表性。 
    为了解决上述问题,我们搜集了近4万条同义词词典,将意思相近的词语由1个词语来替换掉。该优化帮助原本的特征向量减少了3万以上的维度,降低了后续训练模型时所需的内存,而且凝聚了属性的代表性,增加了预测的准确度:

        val sqlContext = new org.apache.spark.sql.SQLContext(sc)
        import sqlContext.implicits._
        val train = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/JBtrain", 400)
        val test = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/JBtest", 400)
        val same = sc.textFile("hdfs://cdh01:8020//user/data/sogou2/same", 400)
        same.filter { x => !x.contains('=') }.count()
        val sameWord = same.map { line =>
          val valuekey = line.split('=')
          (valuekey(1), valuekey(0))
        }.collect()
        val broadcastVar = sc.broadcast(sameWord)
        val diffTrain = train.map { line =>
          val broad = broadcastVar.value
          val regex = """^\d+$""".r
          val temp = line.split("\t")
          val wordArray = temp(4).split(",")
          var str = ""
          for (word <- wordArray) {
            val keyVal = broad.filter(line => line._1.equals(word))
            if (keyVal.length > 0) {
              val oneKeyVal = keyVal(0)
              str = str + "#" + oneKeyVal._2
            } else if (regex.findFirstMatchIn(word) == None) {
              str = str + "#" + word
            }
          }
          (temp(0), temp(1), temp(2), temp(3), str)
        }
        diffTrain.toDF().coalesce(1).write.csv("hdfs://cdh01:8020//user/data/sogou2/ReplaceJBtrain")
    
        val diffTest = test.map { line =>
          val broad = broadcastVar.value
          val regex = """^\d+$""".r
          val temp = line.split("\t")
          val wordArray = temp(1).split(",")
          var str = ""
          for (word <- wordArray) {
            val keyVal = broad.filter(line => line._1.equals(word))
            if (keyVal.length > 0) {
              val oneKeyVal = keyVal(0)
              str = str + "#" + oneKeyVal._2
            } else if (regex.findFirstMatchIn(word) == None) {
              str = str + "#" + word
            }
          }
          (temp(0), str)
        }
        diffTest.toDF().coalesce(1).write.csv("hdfs://cdh01:8020//user/data/sogou2/ReplaceJBtest")
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    4.6 形近词替换

    这里写图片描述 
    设想当一个用户的搜索词列的分词结果中出现了一些形近的词语,如“iPhone5”与“iPhone6”、“杭州”与“杭州站”。该问题和上节所提出的问题类似,为了解决这个问题,我们先来介绍“编辑距离算法”。 
    编辑距离算法,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。例如,计算X字符串 与 Y字符串 的 编辑距离。dp[i][j] 为 X串的前i个字符 和 Y串的前j个字符 的编辑距离:

    if(X [i - 1] == Y[j - 1])       
        dp[i][j] = dp[i - 1][j - 1];   //最后字符相同    
    else
    { 
        int t1 = dp[i - 1][j];                             //删除X第i个字符    
        t1 = t1 < dp[i][j - 1] ? t1 : dp[i][j - 1];        //删除Y第j个字符    
        t1 = t1 < dp[i - 1][j - 1] ? t1 : dp[i - 1][j - 1];//最后字符改相同
        dp[i][j] = t1 + 1;
    }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里我们所使用的优化方案为: 
    这里写图片描述

    • 对整个训练集和测试集的搜索词列做分词后的词频统计表
    • 对每个用户的搜索词列分词后的各个词与词频统计表各词(排除前者自身)进行编辑距离计算。
    • 得到词频统计表中编辑距离与该词编辑距离最小词,在这些词中在选择一个词频最高的词将该词替代。

    4.7 额外增加数据量

    这里写图片描述 
    在大数据时代背景下,只要数据量足够的大,反而我们所选用的不同的算法模型对最终的预测准确率的影响会变小,获取更多数据会使模型更完善更准确。我们这里用不同方案所得到的分词结果,人为的增加训练集的数据。如将10万条记录的训练集进行NLPIR分词得到结果,与进行”结巴”提取关键词得到的结果拼接,就将训练集记录人为的翻倍了。后续的分类实验中证明了,使用该方案,在模型相同的情况下,相比原来会有1%左右的准确率的提升。

    5.聚类与降维

    2009年结束的Nexfix竞赛表明,很多参数团队用到的高等矩阵因子分解对模型提高预测准确略非常有帮助。模型使用矩阵因子分解方法从特征矩阵中抽取一组潜在的属性,并通过这些属性来描述用户。20世纪80年代后期,利用潜在的”语义”属性的思想被成功的应用于信息检索领域。Deerwesteret al. 在1990年提出使用奇异值分解(SVD)方法发现文档中的潜在的属性。[2]而本课题在实验中会使用到LDA方法。

    5.1 LDA

    隐含狄利克雷分配(LDA,Latent Dirichlet Allocation)是一种主题模型(Topic Model,即从所收集的文档中推测主题)。 甚至可以说LDA模型现在已经成为了主题建模中的一个标准,是实践中最成功的主题模型之一。那么何谓“主题”呢?,就是诸如一篇文章、一段话、一个句子所表达的中心思想。不过从统计模型的角度来说, 我们是用一个特定的词频分布来刻画主题的,并认为一篇文章、一段话、一个句子是从一个概率模型中生成的。也就是说 在主题模型中,主题表现为一系列相关的单词,是这些单词的条件概率。形象来说,主题就是一个桶,里面装了出现概率较高的单词(参见下面的图),这些单词与这个主题有很强的相关性。 
    这里写图片描述 
    LDA可以用来识别大规模文档集或语料库中潜藏的主题信息。它采用了词袋的方法,这种方法将每一篇文档视为一个词频向量,从而将文本信息转化为了易于建模的数字信息。但是词袋方法没有考虑词与词之间的顺序,这简化了问题的复杂性,同时也为模型的改进提供了契机。每一篇文档代表了一些主题所构成的一个概率分布,而每一个主题又代表了很多单词所构成的一个概率分布。 
    LDA可以被认为是如下的一个聚类过程:

    • 各个主题(Topics)对应于各类的“质心”,每一篇文档被视为数据集中的一个样本。
    • 主题和文档都被认为存在一个向量空间中,这个向量空间中的每个特征向量都是词频(词袋模型)
    • 与采用传统聚类方法中采用距离公式来衡量不同的是,LDA使用一个基于统计模型的方程,而这个统计模型揭示出这些文档都是怎么产生的。

    5.1.1 模型训练

    Spark API 参数介绍:

    • K:主题数量(或者说聚簇中心数量)
    • maxIterations:EM算法的最大迭代次数,设置足够大的迭代次数非常重要,前期的迭代返回一些无用的(极其相似的)话题,但是继续迭代多次后结果明显改善。我们注意到这对EM算法尤其有效。,至少需要设置20次的迭代,50-100次是更合理的设置,取决于数据集。
    • docConcentration(Dirichlet分布的参数α):文档在主题上分布的先验参数(超参数α)。当前必须大于1,值越大,推断出的分布越平滑。默认为-1,自动设置。
    • topicConcentration(Dirichlet分布的参数β):主题在单词上的先验分布参数。当前必须大于1,值越大,推断出的分布越平滑。默认为-1,自动设置。
    • checkpointInterval:检查点间隔。maxIterations很大的时候,检查点可以帮助减少shuffle文件大小并且可以帮助故障恢复。
        val lda=new LDA()
                    .setK(20)
                    .setOptimizer("online")
                    .setCheckpointInterval(10)
                    .setMaxIter(100)
        val model=lda.fit(dataset_lpa)   
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    5.1.2 模型评价

    生成的model不仅存储了推断的主题,还包括模型的评价方法。模型的评价指标:logLikelihood,logPerplexity。logLikelihood越大越好,logPerplexity越小越好

        val ll = model.logLikelihood(dataset_lpa)
        val lp = model.logPerplexity(dataset_lpa)
     
    • 1
    • 2
    • 1
    • 2

    用评价方法,在online 方法下,对setMaxIter进行调参:

    for(i<-Array(5,10,20,40,60,120,200,500)){
        val lda=new LDA()
                    .setK(3)
                    .setTopicConcentration(3)
                    .setDocConcentration(3)
                    .setOptimizer("online")
                    .setCheckpointInterval(10)
                    .setMaxIter(i)
        val model=lda.fit(dataset_lpa) 
    
        val ll = model.logLikelihood(dataset_lpa) 
        val lp = model.logPerplexity(dataset_lpa)
    
        println(s"$i $ll")
        println(s"$i $lp")
     }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看到,logPerplexity在减小,LogLikelihood在增加,最大迭代次数需要设置50次以上,才能收敛: 
    这里写图片描述 
    这里写图片描述

    5.1.3 对语料的主题进行聚类

        val topicsProb=model.transform(dataset_lpa)
        topicsProb.select("label", "topicDistribution")show(false)
        /** 
            +-----+--------------------------------------------------------------+
            |label|topicDistribution                                             |
            +-----+--------------------------------------------------------------+
            |0.0  |[0.523730754859981,0.006564444943344147,0.46970480019667477]  |
            |1.0  |[0.7825074858166653,0.011001204994496623,0.206491309188838]   |
            |2.0  |[0.2085069748527087,0.005698459472719417,0.785794565674572]   |
            ...
    
        */
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    label是文档序号,文档中各主题的权重,我们可以将该DataFrame带入后续的分类器中,进行训练。

    5.1.4 其他聚类与降维

    Spark在基于RDD的MLlib中还提供了SVD、PCA的降维方法,而基于DataFrame的聚类方法还包括k-means、Bisecting k-means和Gaussian Mixture,其中Gaussian Mixture提供的API类似与LDA,可以直接为我们返回文档中各主题的权重,以便于后续的分类。但是由于LDA在主题聚类上的典型性,我们的课题实验只试验了LDA的方案。

    6. 分类方案

    6.1 Cosine相似度

    假设现在我们有一个测试集特征向量A和一个训练集的特征向量B:

    A:[1, 2, 2, 1, 1, 1, 0] 
    B:[1, 2, 2, 1, 1, 2, 1]

    到这里,问题就变成了如何计算这两个向量的相似程度。我们可以把它们想象成空间中的两条线段,都是从原点([0, 0, …])出发,指向不同的方向。两条线段之间形成一个夹角,如果夹角为0度,意味着方向相同、线段重合;如果夹角为90度,意味着形成直角,方向完全不相似;如果夹角为180度,意味着方向正好相反。因此,我们可以通过夹角的大小,来判断向量的相似程度。夹角越小,就代表越相似。

    这里写图片描述

    以二维空间为例,上图的a和b是两个向量,我们要计算它们的夹角θ。余弦定理告诉我们,可以用下面的公式求得: 

    拓展到n维向量,假定A和B是两个n维向量,A是 [A1, A2, …, An] ,B是 [B1, B2, …, Bn] ,则A与B的夹角θ的余弦等于: 

    使用这个公式,我们就可以得到,特征向量A与特征向量B的夹角的余弦: 

    余弦值越接近1,就表明夹角越接近0度,也就是两个向量越相似,这就叫”余弦相似度” 
    我们这个方案,计算出一条测试集的特征向量与训练集各个特征向量的余弦相似度,将该条测试集的类别标记为与其余弦相似度最大的训练集特征向量所对应的类别。

    6.2 One-vs-Rest

    One-vs-Rest将只能用于二分问题的分类(如Logistic回归、SVM)方法扩展到多类。这种方法基本思想为:

    训练时依次把某个类别的样本归为一类,其他剩余的样本归为另一类,这样k个类别的样本就构造出了k个binary分类器。分类时将未知样本分类为具有最大分类函数值的那类。 
    假如我有四类要划分(也就是4个Label),他们是A、B、C、D。于是在抽取训练集的时候,分别抽取 
    (1)A所对应的向量作为正集,B,C,D所对应的向量作为负集 
    (2)B所对应的向量作为正集,A,C,D所对应的向量作为负集; 
    (3)C所对应的向量作为正集,A,B,D所对应的向量作为负集; 
    (4)D所对应的向量作为正集,A,B,C所对应的向量作为负集; 
    使用这四个训练集分别进行训练,然后的得到四个训练结果文件。在测试的时候,把对应的测试向量分别利用这四个训练结果文件进行测试。最后每个测试都有一个结果f1(x),f2(x),f3(x),f4(x)。于是最终的结果便是这四个值中最大的一个作为分类结果。

    代码实例:

    //定义一个binary分类器,如:LogisticRegression 
    LogisticRegression lr=new LogisticRegression()
                    .setMaxIter(10)
                    .setRegParam(0.3)
                    .setElasticNetParam(0.2)                
                    .setThreshold(0.5);
    //建立一对多多分类器model                
    OneVsRestModel model=new OneVsRest()
                    .setClassifier(lr)//将binary分类器用这种办法加入
                    .fit(training);
    //利用多分类器model预测
    Dataset<Row>predictions=model.transform(test);  
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    很遗憾的是,目前Spark基于DataFrame的MLlib binary分类器中并没有实现SVM,而基于RDD的MLlib有实现SVM,却没有实现One-vs-Rest。

    6.3 朴素贝叶斯

    朴素贝叶斯分类是一种思想比较简单的分类算法,朴素贝叶斯的思想基础是这样的:对于给出的待分类项,求解在此项出现的条件下各个类别出现的概率,哪个最大,就认为此待分类项属于哪个类别。 
    朴素贝叶斯分类的正式定义如下:

    1、设为一个待分类测试特征向量,而每个为属性的值。

    2、有类别集合 

    3、计算 

    4、将该待分类的用户类别标记为

    那么现在的关键就是如何计算第3步中的各个条件概率。我们假设各个特征属性是条件独立的,则根据贝叶斯定理有如下推导:

    因为分母对于所有类别为常数,因为我们只要将分子最大化皆可。又因为各特征属性是条件独立的,所以有:

    而等式右边的各项都可以有训练集求出。以此类推,便可以计算出计算第3步。 
    Spark Mllib实现朴素贝叶斯分类器,Smoothing为平滑系数:

    val lr = new NaiveBayes().setSmoothing(0.1).setFeaturesCol("features")
     
    • 1
    • 1

    6.4 前馈神经网络

    Spark MLlib中实现了MultilayerPerceptronClassifier(MLPC),这是一个基于前馈神经网络的分类器,它是一种在输入层与输出层之间含有一层或多层隐含结点的具有正向传播机制的神经网络模型。

    这里写图片描述

    中间的节点使用sigmoid (logistic)函数,输出层的节点使用softmax函数。输出层的节点的数目表示分类器有几类。MLPC学习过程中使用BP算法,优化问题抽象成logistic loss function并使用L-BFGS进行优化。 
    下面我们来介绍下MultilayerPerceptronClassifier所设计到的参数:

    val lr = new MultilayerPerceptronClassifier().setMaxIter(51).setLayers(Array[Int](vector_len, 6, 5, classfiy_num)).setSeed(1234L).setFeaturesCol("features").setLabelCol("label").setPredictionCol("prediction")
     
    • 1
    • 1
    • layers:各层的节点数,第1层的个数必须为特征向量的维度数,最后1层的个数必须为类别数,中间为各隐藏层的节点数,可以任意设置。对于隐藏成节点数的优化,我们做了一下的方案:由于我们的年龄、性别、学历的类别数分别是6、2、6。假设我们在预测年龄时,我们可以将隐藏的第1层节点数设为6 * 2 * 6 = 72,即根据先将其分为3个label都互斥的类别,。然后,将隐藏的第2层节点数设为6 * 2 = 12,即根据先将其分为年龄和性别3个label都互斥的类别。最后,再分到我们想预测的年龄的6个类别上。
    • maxIter:最大迭代次数。进行参数调优时,主要是在优化这个参数。
    • Seed:随机种子
    • tol:(代码中未设置)允许误差。一般取0.001~0.00001,当迭代结果的误差小于该值时,结束迭代计算,给出结果。
    • solver:(代码中未设置)优化器。有两种算法可供选择: l-bfgs和gd。默认为 l-bfgs算法,因为gd算法比l-bfgs算法需要多得多的迭代次数,即使在提高学习步长和提高允许误差tol的情况下,还是慢很多:

      .stepSize=0.03,tol=0.0001 
      l-bfgs:上很快能收敛,大约20次,训练速度也更快 
      maxIter = 5 accuracy = 0.35 training time = 267ms 
      maxIter = 10 accuracy = 0.74 training time = 429ms 
      maxIter = 20 accuracy = 0.91 training time = 657ms 
      maxIter = 50 accuracy = 0.92 training time = 941ms 
      maxIter = 100 accuracy = 0.92 training time = 914ms 
      maxIter = 500 accuracy = 0.92 training time = 1052ms

      这里写图片描述

      gd算法:基本上要比l-bfgs算法慢10以上 
      stepsize=0.2,tol=0.001 
      maxIter = 100 accuracy = 0.55 training time = 4209ms 
      maxIter = 500 accuracy = 0.92 training time = 11216ms 
      maxIter = 1000 accuracy = 0.92 training time = 14540ms 
      maxIter = 2000 accuracy = 0.92 training time = 14708ms 
      maxIter = 5000 accuracy = 0.92 training time = 14669ms

    • stepSize:学习步长。一般来说学习步长越大,权重变化越大,收敛越快;但训练速率过大,会引起系统的振荡。太高的学习步长,可以减少网络训练的时间,但是容易导致网络的不稳定与训练误差的增加,会引起系统的振荡。

    6.5 预分类迭代预测

    这里写图片描述 
    数据表明,当我们在进行对年龄进行分类时,其实同一年龄段的男性和女性查询语句是存在较大的差异的。我们可以借鉴6.4 前馈神经网络中对layers(各层的节点数)优化的类似的思想: 
    1. 将测试集进行性别男女的预测分类,预测成男性的分为测试集test1,预测成女性的分为测试集test2。 
    2. 将训练集根据性别划分为train1(男性)和train2(女性)。 
    3. 分别用train1训练模型来预测test1的年龄,train2训练模型来预测test2年龄。 
    上诉思想可以实现一种迭代,即继续对年龄的预测结果进行划分来预测学历,再对学历的预测结果进行划分来预测性别,在进行上诉的第2、第3步,如此反复继续。 
    这个方案由于将数据集划分了,每次降低了数据量,这会对准确率有负面的影响,但最终总体预测准确率提高了0.3%左右。

    6.6 Boosting

    Boosting分类器属于集成学习模型,它基本思想是把成百上千个分类准确率较低的树模型组合起来,成为一个准确率很高的模型。这个模型会不断地迭代,每次迭代就生成一颗新的树。

    6.6.1 xgboost

    xgboost 的全称是eXtreme Gradient Boosting。它是Gradient Boosting Machine的一个c++实现,作者为华盛顿大学研究机器学习专家陈天奇 。他在研究中深感自己受制于现有库的计算速度和精度,因此开始着手搭建xgboost项目。xgboost最大的特点在于,它能够自动利用CPU的多线程进行并行,同时在算法上加以改进提高了精度。在Kaggle的希格斯子信号识别竞赛中,它因为出众的效率与较高的预测准确度在比赛论坛中引起了参赛选手的 广泛关注 ,在1700多支队伍的激烈竞争中占有一席之地。 
    xgboost拥有自身的jvm项目包,可以和Spark集成。很遗憾的是,由于时间关系,我们并没有成功尝试这个方案,而是在已有分类的结果上实现了简单的加权投票的策略,最终总体预测准确率提高了0.4%左右。

    7. 参数调优

    7.1 交叉验证法

    Spark Mllib 中实现的是留一法交叉验证法。留一法交叉验证法的思想是:将原来的训练集有N个数据集,将每一个数据集作为测试集,其它N-1个数据集作为训练集。这样得到N个分类器,N个测试结果。用这N个结果的平均值来衡量模型的性能:

    val pipeline = new Pipeline()
      .setStages(Array(tokenizer, hashingTF, lr))
    
    val paramGrid = new ParamGridBuilder()
      .addGrid(hashingTF.numFeatures, Array(10, 100, 1000))
      .addGrid(lr.regParam, Array(0.1, 0.01))
      .build()
    
    val cv = new CrossValidator()
      .setEstimator(pipeline)
      .setEvaluator(new BinaryClassificationEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setNumFolds(2)  
    
    val cvModel = cv.fit(training)
    
    ......
    
    cvModel.transform(test)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    参数介绍:

    • Estimator:即所要进行评估的模型
    • Evaluator:对模型评估器,可以是二分类的BinaryClassificationEvaluator 或是 多分类的 MulticlassClassificationEvaluator
    • EstimatorParamMaps:模型参数表。可以由ParamGridBuilder调用addGrid方法对模型的某个参数设置一组需要验证的值,再调用build()返回。
    • NumFolds:即所要划分的数据集的数量N

    7.2 划分训练集验证法

    划分训练集验证法的思想比较简单,我们将训练集按 m:1 - m 的比例划分成两个部分,第1部分作为新的训练集,第2部分作为验证集:

    val trainValidationSplit = new TrainValidationSplit()
      .setEstimator(lr)
      .setEvaluator(new RegressionEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setTrainRatio(0.8)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 1
    • 2
    • 3
    • 4
    • 5

    我们可以从上述代码看到参数基本上和交叉验证法相同,其中TrainRatio就是我们说的m。 
    划分训练集验证法优点在于所需要的时间较短。当我们在对前馈神经网络参数调优时,因为耗时过长而无法选用交叉验证法,而划分训练集验证法则是一种很好的替代方案。 
    很遗憾的是,Spark Mllib所实现的交叉验证法和划分训练集验证法都没有返回验证所选得的一组最优参数的API,而是将其视为一种模型直接对原始训练集进行训练,最后返回预测结果。而且,划分训练集验证法只对训练集划分一次进行预测,这具有很大的偶然性。由于以上的原因,我们动手自己实现了划分训练集验证法,并每次验证进行了三次的随机划分和训练,以其平均值作为验证的结果,最后按准确率对参数组降序排序。:

       val evaluations =
          for (
            smooth <- 1 to 100
          ) yield {
            val lr = new NaiveBayes().setSmoothing(smooth.toDouble / 100.0).setFeaturesCol("features")
            val pipeline = new Pipeline().setStages(Array(lr))
            var sumA = 0.0
            for (cnt <- 1 to 3) {
              val Array(trainData, testData) = dataIDF.randomSplit(Array(0.7, 0.3))
              trainData.cache()
              testData.cache()
              val model = pipeline.fit(trainData)
              val predictions = model.transform(testData)
              val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label").setPredictionCol("prediction").setMetricName("accuracy")
              val accuracy = evaluator.evaluate(predictions)
              sumA = sumA + accuracy
            }
            val allAccuracy = sumA / 3.0
            println(((smooth), allAccuracy))
            ((smooth), allAccuracy)
          }
        evaluations.sortBy(_._2).reverse.foreach(println)
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    使用上述代码对贝叶斯分类器关于各个label预测的参数(平滑度)调优,总体平均准确率相比原来有6%左右的提升。

    8. 回顾与展望

    8.1 总结

    经过我们对各个分词、特征提取与转换、聚类、分类以及参数调优的方案的尝试,最终我们选择了:

    • 分词:NLPIR分词、结巴TF-IDF与结巴TextRank叠加,将训练集数据量变为原来的3倍。
    • 特征提取与转换:选择了HashingTF,维度设置为10万。
    • 聚类:弃用。
    • 分类:使用前馈神经网络,对每个Label预测,设置参数都为:
    MultilayerPerceptronClassifier().setMaxIter(32).setLayers(Array[Int](vector_len, 6, 5, classfiy_num))
    
     
    • 1
    • 2
    • 1
    • 2
    • 参数调优:将HashingTF维度降低为1万,使用划分训练集验证法进行调优。

    该方案所能达到的平均准确率为69.652%,再对其他方案与该方案进行简单的加权投票,平均准确率可达70%左右。

    8.2 不足与展望

    从上节可以发现,很遗憾的我们在最终的方案中做了很多原本优化上的妥协:

    • 分词:从最终分词结果的数据上表明,分词结果还存在非常大可以改进的地方,还存在许多代表性极弱的词语。
    • 特征提取与转换:由于模型的选择,试验证明使用IDF和权值规范化,并不能得到预期的效果。反而,只使用CountVectorizer会得到较好的效果。而在选用前馈神经网络分类器后,Spark机器无法支撑CountVectorizer如此大的维度(按结巴TF-IDF分词结果,使用CountVectorizer会得到维度为60多万的特征向量,而去词频为1的词语后得到的特征向量维度为30多万),因而我们退而求其次选用了HashingTF。
    • 聚类:实验中的Spark集群使用LDA,所能承受特征维度在30万以下,影响了LDA的效果。而训练一次(迭代100次)所花费的时间,我们实在无法承受,再而我们对参数的设置(如主题个数、迭代次数)并没太大的经验。在多次试验,得到后续的分类效果并不良好的情况下,我们放弃了这个方案。
    • 分类与参数调优:当我们弃用朴素贝叶斯分类器,转而试用并未进行参数调优的前馈神经网络分类器后,惊喜的发现后者比前者参数调优过的平均预测准确率还要高出4%左右。但很遗憾的是,就算我们将特征向量维度降低至10万,对前馈神经网络分类器的参数调优所耗费的时间还是远远的超出我们所能承受的。于是,我们尝试将维度降低至1万,调优得到参数组后,再带回到10万维度的特征向量;尝试减少数据量进行参数调优。而结果证明,这两种方案的调优都不能与使用原始的特征向量调优效果一致。
    • xgboost:由于时间关系,我们并没有成功尝试这个方案,而是在已有分类的结果上实现了简单的加权投票的策略。

    由于我们的经验不足,再加上实验室的项目任务、在校课程的学习任务以及后期转而对Spark Core源码的研究,使得原本可以做的优化并没有精力去很好的实现。希望在后续能将其完善的更好。

    8.3 感谢

    感谢实验室所提供的服务器集群环境,感谢同组同学在一起交流讨论中激发出灵感。非常感谢这次课题实验给我带来的学习机会,让我从头到尾自主的完成了一次数据处理、分析的过程,也深深的感受到了Spark的魅力和大数据处理的重要性,也坚定了我从事Spark大数据处理与分析研究的决心。

    9. 参考文献

    【1】Mihalcea R, Tarau P. TextRank: Bringing order into texts[C]. Association for Computational Linguistics, 2004. 
    【2】Dietmar J, Markus Z. Recommender systems: An introduction[M]. Cambridge University Press, 2010: 35.

    展开全文
  • 智慧方案
  • 使用TFIDF特征值对用户查询历史中的词向量进行加权平均,可以得到表示整体查询的向量值,可以将其直接作为多个分类模型的输入,完成用户层级的分类任务。 2.4. Doc2Vec特征表示 为了将文档直接表示成一个...

    作者:李恒超,李裕礞,王安然,钱凌飞,任璐,林鸿飞 ——大大黑楼战队

    目录
    1. 数据预处理
    1.1. 停用词处理
    1.2. 分词
    2. 特征表示
    2.1. Bag of Words
    2.2. Word Embedding
    2.3. Topical Word Embedding
    2.4. Doc2Vec特征表示
    2.5. 人工构建的特征
    3. 模型结构
    3.1. 基于TFIDF的传统机器学习模型
    3.2. 基于分布式向量的神经网络模型
    3.3. 第二层融合模型
    4. 数据后处理——错误分析
    5. 总结与展望
    5.1. 深度学习方法
    5.2. 查询扩展与为相关反馈方法

    1. 数据预处理

    1.1. 停用词处理

    在各类文本相关的任务中,大多需要先对样本进行分词、去停用词等预置处理。在本任务中,通过对训练数据进行细致的分析,结合人们进行日常检索的先验知识,发现“空格”、“标点”及很多停用词均有助于判别用户的基本属性。

    因此在进行分词处理及TFIDF特征计算的过程中,均保留了空格、标点以及停用词这些信息,在该任务中是很好的特征。比如我们发现,一个人的受教育程度越高,越喜欢在查询词中添加空格,例如“大连理工大学 图书馆 地址”;而受教育程度越低,使用“之”的概率就越大,因为“之”经常在一些玄幻修真小说名中出现,低年龄和低学历会经常搜类似的小说如《网游之邪龙逆天》。

    1.2. 分词处理

    我们发现不限制字典长度才能达到最好的预测效果,由于电脑内存的限制,我们的Bigrams过滤掉了文档频率低于5的词,整个字典长度是174W。我们分析该语料的特点是低频词特别多,而且这些低频词有很好的预测效力。

    该任务中的文本为用户的查询记录,其长度往往较短,因此分词效果便显得较为重要。本文测试了几种常用的分词工具,并使用Bayes模型比较了它们在本任务中的效果,下表展示了其中表现较好的几组结果:

    分词工具学历年龄性别
    JIEBA58.55%54.35%81.83%
    NLPIR57.57%53.84%81.12%
    THULC58.54%54.14%81.26%
    Ngram(1,2)58.03%53.87%81.11%

    下表是分词效果:

    原单词JIEBATHULCNLPIR
    周公解梦大全查询周公, 解梦, 大全, 查询周公解, 梦, 大全, 查询周, 公, 解, 梦, 大全, 查询
    中财网首页中财网,首页中财网,首,页中,财网,首,页

    2. 特征表示

    为了对数据进行更全面的刻画,我们从用户用词习惯、语义相关性及所包含的主题几方面构建了多角度的特征。具体特征如下:

    2.1. Bag of Words

    我们筛选了至少出现在5篇文档中的词语来组成词表,统计one-gram及bi-gram特征。该特征可以有效体现出不同类别用户的用词习惯。

    该特征虽然简单且有效,但其缺陷在于其缺乏词与词之间的语义相关信息,因此我们在文本语义方面采用了更多的特征表示方法。

    2.2. Word Embedding

    我们使用Google公布的word2vec工具在搜狗新闻语料上训练得到了常用词的词向量,并将其应用到用户的历史查询词中。该方法得到的词向量可以有效计算出两个词之间语义的相似程度,从而表示出不同用户查询历史的差异。

    2.3. Topical Word Embedding

    在该任务中,每个用户具有多组查询词,其中有些查询相关性较强,有些则完全不相关。使用主题模型来抽取用户的多个查询主题,更有利于刻画用户的查询习惯、关注方向等。我们将用户的所有查询词拼接在一起,使用LDA模型进行主题分析。基于LDA的结果,使用Topical Word Embedding(TWE)[2]模型训练得到每个查询词的词向量。TWE模型与常用的Word2Vec不同在于,其计算出的词向量同时考虑词的上下文及该词所在主题的信息。使用TFIDF特征值对用户查询历史中的词向量进行加权平均,可以得到表示整体查询的向量值,可以将其直接作为多个分类模型的输入,完成用户层级的分类任务。

    2.4. Doc2Vec特征表示

    为了将文档直接表示成一个固定长度的向量,我们还采用了Doc2Vec[1]方法,它通过直接构造文档向量,并将该向量加入到该文档中词向量的训练过程,进行共同训练,从而得到能直接体现该文档语义特征的向量。根据训练文档向量的网络结构不同,可分为Distributed Memory Model(DM)与Distributed bag of words(DBOW)两种模型,其中DM模型不仅考虑了词的上下文语义特征,还考虑到了词序信息。DBOW模型则忽略了上下文词序信息,而专注于文档中的各个词的语义信息。我们同时采用了DM和DBOW这两种文档向量表示方法,从而保证构建的特征中信息的完整性。

    图1 DM模型结构图图2  DBOW模型结构图
    图1 DM模型结构图 图2 DBOW模型结构图

    以下在3个子任务上使用对doc2vec表示向量使用tsne降维的效果:

    图3 教育任务上的效果图4 年龄任务表现效果图5 性别任务表现效果
    图3 教育任务上的效果 图4 年龄任务表现效果 图5 性别任务表现效果

    2.5. 人工构建的特征

    除以上通过模型学习得到的特征表示之外,我们还人工构建了一些特征,从而完成多视角特征体系的构建。人工构建的特征包括:查询词的个数、查询词的平均长度、查询词的最大长度、有空格的query占总查询的比例、英文查询词的比例、所有query的语义标准差(该特征用来表示用户查询的多样化程度)。

    3. 模型结构

    图6 模型整体结构图
    图6 模型整体结构图

    图6中给出了第一赛季和第二赛季的综合模型结构图,其中使用了两级的模型结构。第一级中使用了传统机器学习模型与TFIDF特征相结合来抽取用户用词习惯的差异,使用神经网络模型与表示学习相结合来抽取查询的语义关联信息。第二级模型中使用了XGB Tree模型及Stacking多模型融合的方法,来进一步提升模型的准确性与泛化能力。
    图7 多输出神经网络结构图图8 多模型融合数据分割图
    图7 多输出神经网络结构图 图8 多模型融合数据分割图

    3.1. 基于TFIDF的传统机器学习模型

    第一层模型中,我们尝试了sklearn中的LogisticRegression, MultinomialNB, BernoulliNB, KNN, SVC, RandomForestClassfier 和 xgboost中的gblinear和gbtree。其中,由于tfidf特征过于稀疏、维度过高,树形模型表现效果很差;由于数据量太大,KNN和SVC算法都不能训练出结果;Gblinear线下测试要高于逻辑回归,但是线上成绩不如逻辑回归。

                                    表1   传统机器学习模型的线下成绩
    
    分类器学历年龄性别线下成绩
    LogisticRegression0.64240.59780.83380.6913
    Gblinear0.64840.60680.83620.697
    MultinomialNB0.61210.57980.82620.6727
    BernoulliNB0.60320.57120.82460.6663

    3.2. 基于分布式向量的神经网络模型

    我们同时使用了Doc2Vec与TWE进行用户查询词的表示。我们发现在该语料上,Doc2Vec的表现对迭代次数和学习率很有关系,所有我们使用5折交叉验证分别选择pv-dbow和pv-dm的迭代次数和学习率。其中pv-dbow的迭代2次,学习率为0.025,pv-dm的迭代次数为10次,学习率是0.05;其中NN的参数是300维的隐藏层;TWE中主题数400,alpha为0.5,beta为0.1,输出向量维度400维。以下是一些实验数据:

                                    表2   神经网络模型的线下成绩
    
    分类器学历年龄性别线下成绩
    Dbow-lr0.63300.59470.83710.6883
    Dm-lr0.63700.59180.83490.6879
    Dbow-NN0.66260.61720.84290.7076
    Dm-NN0.65060.60960.83830.6995
    TWE-multiNN0.63010.59900.83500.6880

    在该语料上,Doc2Vec的表现特别好,我们分析原因可能有2点:
    1. 用户的查询词千差万别,相比于一般常见的电影评论、新闻等数据集,该语料的低频词特别多。Doc2vec能够对低频词有很好的语义总结,对这些低频词利用更充分;
    2. 该语料是很多查询词的拼接,语料中的词序(word order)特征不怎么重要,pv-dbow的训练方式忽略了词序,天然地适合处理该语料。

    3.3. 第二层融合模型

    我们的融合技术很大程度上参考了Combining Predictions for Accurate Recommender Systems [7], Ensemble of Generative and Discriminative Techniques for Sentiment Analysis of Movie Reviews [8]这两篇论文。

    第一层模型分别在3个子任务上训练,模型输出的概率值作为下一层模型的输入。由于3个子任务分别是6分类、6分类和2分类,所以我们第一层特征维数是4*(6+6+2)= 56。下表是一些实验结果:

                                    表3    多任务多模型融合的线上成绩 
    
    模型学历年龄性别线下成绩线上成绩
    Tfidf-ensemble0.64220.61040.83280.69510.7101
    Dbow-ensemble0.67170.63320.84920.71800.7116
    Tfidf-dm-ensemble0.67080.63320.84810.71740.7213
    Tfidf-dbow-dm-ensemble0.67880.63890.85160.72310.7246
    Tfidf-dbow-dm-twe-ensemble0.67900.63850.85200.72320.7255

    Tfidf-ens和Dbow-ens是在3个子任务分别训练tfidf-lr模型,然后使用xgboost融合;tfidf-dm-ens是指3个子任务上分别训练tfidf-lr模型和dbow-nn模型,然后使用xgboost融合;Tfidf-dbow-dm-ens是在3个子任务上分别训练tfidf-lr、dbow-nn、dm-nn模型,然后使用xgboost融合;tfidf-dbow-dm-twe-ens是融合了tfidf-lr、dbow-nn、dm-nn、twe的模型。

    在该任务上,融合技术非常关键,我们只用tfdif-lr模型然后在其之上使用xgboost融合,就能达到0.710线上成绩,这样的成绩差不多就能进前5名了。融合技术之所有如此关键,我们分析有以下3点:
    1. 在多分类任务中,一般模型是基于OneVsRest或者OneVsOne,这样分类器只能看到2类的分类信息,而使用stack技术输出每一类的概率值后,第二层模型可以看到所有的分类结果,然后在其之上做一些阈值判断、相互校验等等。2分类任务性别的融合效果不如其它2个6分类任务也验证了这一点。
    2. 3个子任务之间是有一些关联的,比如年龄和学历之间有很大的关联。第二层的模型能对这样的特征关系有很好的学习。例如,我们试过在预测学历时去掉年龄和性别特征,预测结果有一定程度的降低。
    3. 该数据集存在数据不均衡的问题,但由于评价指标是准确率(acc),所以downsample和upsample都没有必要做。借由xgboost模型,我们可以很好的学习各个类别中最优的阈值。

    4. 数据后置处理——错误分析

    错误样本分析可以给模型优化指引方向。在进行错误样本分析的过程中,我们也找到了一些规律。
    对于属性值存在空缺的样本,我们首先使用属性值已知的样本作为训练样本,使用LR模型训练分类器,再对这部分属性空缺样本进行预测,从而补全空缺值。但我们发现在最终的两级多模型融合得到的结果中,对于教育属性空缺的样例,它们的年龄和性别预测准确率很低;对于年龄属性空缺的样例,教育预测准确率很低;对于性别属性空缺的样例,教育预测准确率很低。具体比较结果如下:
    教育属性空缺与未空缺部分的预测准确率比较年龄属性空缺与未空缺部分的预测准确率比较
    教育属性空缺与未空缺部分的预测准确率比较 年龄属性空缺与未空缺部分的预测准确率比较
    性别属性空缺与未空缺部分的预测准确率比较各属性空缺值占比
    性别属性空缺与未空缺部分的预测准确率比较 各属性空缺值占比

    以年龄属性空缺的样本为例,其共包含样本1670个,其中教育属性预测正确的比例为14.59%;而对于其他年龄属性未空缺的样本,其教育属性预测正确的比例为68.38%,差距为53.79%。由此可推测出:存在空缺值的样本,它们的标注质量较差。结合先验知识,我们也可直观地想到,用户的这三项基本属性存在空缺,可能意味着用户信息统计较不充分、信息来源可靠性较差。因该部分样本噪音较大,可能干扰分类器的训练,因此本文最后将存在空缺的样本从训练集中删掉,这样训练出的模型较使用LR填充空缺属性值在最终评价指标上有0.1%~0.2%的提升。

    5. 总结与展望

    本次竞赛极大地锻炼了我们的团队协作及解决问题的能力,有机会将学习的理论真正应用起来。在竞赛中我们还尝试了很多其他方法,但因一些条件的限制,无法调至最优的水平,其中较有参考意义的方法包括深度学习模型及查询扩展方法。

    5.1. 深度学习方法

    目前深度学习模型发展迅猛,已经应用到自然语言处理中的多项任务中。本文也尝试采用深度学习模型来解决此问题。使用Word2Vec训练得到的词向量,输入到CNN[6]模型中,经Pooling层及Softmax分类层,最后输出结果。另外尝试了基于层级式Attention[5]机制[3]的深度神经网络模型,该模型在微博语料上取得了很好的效果,但在本任务中并无突出表现。
    分析原因在于,使用搜狗新闻语料训练得到的词向量只能覆盖本语料中词表的18%,其中大量检索词无法找到,包括人名、地名及生僻字等;另外,本任务中的文本为查询关键词,并非完整的句子,其语义的不连贯性及表述的不全面性也导致了基于语义词向量的深度神经网络模型表现不佳。

    5.2. 查询扩展与伪相关反馈方法

    因查询关键词语义不完整,我们考虑使用搜狗新闻语料及查询扩展方法,补充查询关键词的信息。通过使用BM25F相似度计算方法对训练集和测试集的每条查询在新闻语料中进行搜索匹配,筛选出其中匹配得分最高的前n条新闻。分别使用这些新闻的标题、内容、以及类别信息,对用户查询关键词进行特征补充,从而增加用户文本的信息量,其新闻类别又可起到特征降维的作用。

    在实验过程中发现,由于新闻语料并不充分,内容具有局限性,在该语料上检索得到的结果含有较大噪声,在模型中拼接该部分特征并无明显提升,但该方法也可作为解决该任务的一种思路,未来我们也将继续尝试这部分的工作。

    github项目地址:https://github.com/hengchao0248/ccf2016_sougou

    参考文献

    [1] Le, Quoc V., and Tomas Mikolov. “Distributed Representations of Sentences and Documents.” ICML. Vol. 14. 2014.
    [2] Liu, Yang, et al. “Topical Word Embeddings.” AAAI. 2015.
    [3] Blei, David M., Andrew Y. Ng, and Michael I. Jordan. “Latent dirichlet allocation.” Journal of machine Learning research 3.Jan (2003): 993-1022.
    [4] Mikolov, Tomas, et al. “Efficient estimation of word representations in vector space.” arXiv preprint arXiv:1301.3781 (2013).
    [5] Yang, Zichao, et al. “Hierarchical attention networks for document classification .” Proceedings of the 2016 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies. 2016.
    [6] Kim, Yoon. “Convolutional neural networks for sentence classification.” arXiv preprint arXiv:1408.5882 (2014).
    [7] Jahrer M, Töscher A, Legenstein R. Combining predictions for accurate recommender systems[C]//Proceedings of the 16th ACM SIGKDD international conference on Knowledge discovery and data mining. ACM, 2010: 693-702.
    [8] Mesnil G, Mikolov T, Ranzato M A, et al. Ensemble of generative and discriminative techniques for sentiment analysis of movie reviews[J]. arXiv preprint arXiv:1412.5335, 2014.

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,504
精华内容 5,401
关键字:

用户画像挖掘