您的位置:首页 > 其它

服务产品(商品)评论中的产品特征挖掘方法

2016-10-13 10:39 232 查看
也是好久没写博客了,前段时间一直在找工作,没有做什么实质性的工作。最近工作也定下了,百度流量质量控制部的反作弊算法团队,不算是百度的什么土豪团队,但是99%以上的流量收入都是要从这个团队过一遍的,团队资历实力可见一斑。

好了不吹b了,说说这个阶段要做的东西:从服务产品的评论中挖掘服务产品的特征,不理解的话举个例子:

“这饭店环境还真是不错,就是菜码有点太大了!”

很明显,加了高亮部分的文字蕴含着这个服务产品的两个特征,那么我要做的其实就是把这样的特征找出来,可以归结于数据挖掘范畴。

这个事做成了之后要做什么我先不说,因为这是我的毕业课题,透露太多了也不好。博客上我只会放处理的大致流程和遇到的问题,源码部分公开。

做学术研究嘛肯定是要有数据源的,数据源老师给提供了一个,即Yelp Dataset Challenge中的数据源:https://www.yelp.com/dataset_challenge  感兴趣的朋友可以去看看,数据质量非常高,略强于阿里天池。

(二)英文分词、赋词性

我之前做的都是中文分词,看到是英文分词给我开心坏了,空格不都打好的么。赋词性这块就不行了,因为不可能搞个词典挨个去查,我就用了nltk英文NLP处理包,这玩意以前没用过,具体代码在最后面放着,注释非常全,自己研究就行了。下面我主要说一说nltk的词性标注,这个标注还是挺奇怪的,在官网没有找到词性标注表,跟国内的北大几级标注那些又不一样,去翻了一下http://blog.csdn.net/heyongluoyao8/article/details/43731743#reply这个人的博客,做个归纳,方便以后查找。

1.     CC      Coordinating conjunction 连接词

2.     CD     Cardinal number  基数词

3.     DT     Determiner  限定词(如this,that,these,those,such,不定限定词:no,some,any,each,every,enough,either,neither,all,both,half,several,many,much,(a) few,(a) little,other,another.

4.     EX     Existential there 存在句

5.     FW     Foreign word 外来词

6.     IN     Preposition or subordinating conjunction 介词或从属连词

7.     JJ     Adjective 形容词或序数词

8.     JJR     Adjective, comparative 形容词比较级

9.     JJS     Adjective, superlative 形容词最高级

10.     LS     List item marker 列表标示

11.     MD     Modal 情态助动词

12.     NN     Noun, singular or mass 常用名词 单数形式

13.     NNS     Noun, plural  常用名词 复数形式

14.     NNP     Proper noun, singular  专有名词,单数形式

15.     NNPS     Proper noun, plural  专有名词,复数形式

16.     PDT     Predeterminer 前位限定词

17.     POS     Possessive ending 所有格结束词

18.     PRP     Personal pronoun 人称代词

19.     PRP$     Possessive pronoun 所有格代名词

20.     RB     Adverb 副词

21.     RBR     Adverb, comparative 副词比较级

22.     RBS     Adverb, superlative 副词最高级

23.     RP     Particle 小品词

24.     SYM     Symbol 符号

25.     TO     to 作为介词或不定式格式

26.     UH     Interjection 感叹词

27.     VB     Verb, base form 动词基本形式

28.     VBD     Verb, past tense 动词过去式

29.     VBG     Verb, gerund or present participle 动名词和现在分词

30.     VBN     Verb, past participle 过去分词

31.     VBP     Verb, non-3rd person singular present 动词非第三人称单数

32.     VBZ     Verb, 3rd person singular present 动词第三人称单数

33.     WDT     Wh-determiner 限定词(如关系限定词:whose,which.疑问限定词:what,which,whose.)

34.     WP      Wh-pronoun 代词(who whose which)

35.     WP$     Possessive wh-pronoun 所有格代词

36.     WRB     Wh-adverb   疑问代词(how where when)

(二)挖掘产品特征(10.13 Baseline)

挖掘这些特征就要分析自然语言的结构了,在上面的例子中(分词后):这/ 饭店/环境/还/ 真是/ 不错,就是/
菜码/有点/ 太大/ 了!我们可以很清楚的发现,表特征的词往往都是名词(NN/NNS),而这些特征的周围必定跟随一个形容词(也可能连带一个或几个程度副词),看了一下Yelp的评论数据也确实如此。根据这个线索就可以编码了。

编码的时候要注意一个问题就是先找形容词(JJ/JJR/JJS)再找特征,但是表程度的这个形容词往往不在这个特征的旁边,那么久要设置一个滑窗,在滑窗范围内寻找这个特征(我暂时设置的是滑窗=5),这种办法个人感觉简单粗暴,但是缺点是只能找单个词的特征,无法寻找一类短语特征(比如dish size),这个问题在以后的探索中慢慢解决。现在先不着急,先搞个baseline看看效果怎么样再说,我把餐饮行业的feature都拿到了,一共92326个,feature出现次数在1000以下的我认为是稀疏特征,直接扔掉,剩下494个特征,我取前50个展示一下吧:

('food', 203900)
('place', 126807)
('service', 110508)
('time', 98767)
('restaurant', 47986)
('staff', 41384)
('menu', 36441)
('experience', 35822)
('meal', 32812)
('thing', 32057)
('night', 30665)
('sauce', 28831)
('bit', 28180)
('pizza', 26407)
('order', 24406)
('bar', 24077)
('price', 23429)
('chicken', 22599)
('bread', 21457)
('hour', 21134)
('dish', 20586)
('side', 20291)
('way', 20077)
('lunch', 19849)
('flavor', 19529)
('dinner', 19046)
('day', 18674)
('server', 18649)
('salad', 18648)
('rice', 18287)
('nothing', 17406)
('something', 17184)
('selection', 16780)
('quality', 16727)
('everything', 15665)
('meat', 15177)
('table', 15061)
('location', 15022)
('atmosphere', 14962)
('cheese', 14741)
('steak', 14428)
('sushi', 14326)
('area', 14032)
('taste', 13685)
('breakfast', 13576)
('visit', 13576)
('waitress', 12394)
('soup', 12300)
('sandwich', 12280)
('beer', 12261)


个人感觉这种baseline算法还是非常靠谱的,我们能看到的都是真真切切的特征,没有噪音。

(三)有什么改进的地方

这50个特征有很多是一类,比如soup和sandwich,都表示的餐饮中的一个菜,那么未来能否使用wiki大规模语料库将这些近义词合并成一个类目会成为一个比较大的工作量;再有就是挖出来的feature都是一个词,看看能不能挖出一个短语吧。

(四)源码

# coding:utf-8

import nltk, os, sys

#
#  进度条
#
def View_Bar(flag, sum):
rate = float(flag) / sum
rate_num = rate * 100
if flag % 15.0 == 0:
print '\r%.2f%%: ' %(rate_num), # \r%.2f后面跟的两个百分号会输出一个'%'
sys.stdout.flush()

#
#  筛选 指定类型的 店家
#
def Filter_Business(path, type): # path是business表路径,type:待筛选店家的categories
lines = open(path, 'r').readlines()
filter_bussiness_id = {} # 筛选完成的商家id的字典 {"5UmKMjUEUNdYWqANhGckJw":None, "UsFtqoBl7naz8AVUBZMjQQ":None, }

flag = 0 # 进度条
for line in lines:
line = line.split('"categories": ')
categories = line[1].split(', "city"')[0] # ["Fast Food", "Restaurants"]
business_id = line[0].split(', "full_address"')[0].split('"business_id": ')[1]
if type in categories:
filter_bussiness_id[business_id[1:-1]] = 0
flag += 1
print ('筛选数据进度: ' + str(flag/85901.0))

print ('字典: ' + str(filter_bussiness_id))
print ('字典长度: ' + str(len(filter_bussiness_id)))

return filter_bussiness_id

#
#  提取指定类型店家的 review
#
def Filter_Business_Review(path, filter_business_id): # path是review文件路径
lines = open(path, 'r').readlines()
f = open('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/review_text.txt', 'w')

flag = 0 # 进度条
for line in lines:
dict = eval(line)
business_id = dict['business_id'] # 该评论对应的商家
if filter_business_id.has_key(business_id) == True:
text = dict['text'].replace('\n', '')
f.write(text + '\n')
flag += 1
print (flag/2685066.0)

return 0

#            #
#  词性标注  #
#            #
def Tag_Word(path): # path 是所有用户的评论文件路径
lines = open(path, 'r').readlines()
tags = [] # 保存每个文章分词后的词性 [ [('Excellent', 'JJ'), ('food', 'NN'), ('.', '.')],
#                           [('Superb', 'NNP'), ('customer', 'NN'), ('service', 'NN'), ('.', '.')] ]
feature_word = [] # 提出的服务价值分布特征

# 分词、赋词性
f = open('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/word_tagged_sentences.txt', 'w')  # 保存一下词性标注后的结果
flag = 0 # 进度条
for text in lines:
sentences = nltk.sent_tokenize(text) # 将文本拆分成句子列表
# 先对每个句子进行分词,在对这个句子进行词性标注(这样效果比较好)
for sentence in sentences:
word = nltk.word_tokenize(sentence) # 先对句子进行分词 ['Excellent', 'food', '.']
word_tagged = nltk.pos_tag(word) # 再对这个分好的句子进行词性标注 [('Excellent', 'JJ'), ('food', 'NN'), ('.', '.')]
for item in word_tagged: # 将标注好的词写入文件中
f.write(item[0] + '/' + item[1] + ' ') # 'Excellent/JJ food/NN ./. '
f.write('\n') # 这里我认为每个能展现feature的评论都是蕴含在一句话中的,因此每句话一行,到时候找feature的时候也是一行一行的去找
flag += 1
print ('分词进度: ' + str(flag/2687201.0))

return 0

#                     #
#  筛选 feature 词汇  #
#                     #
def Featuer_Word(path, window): # path 是词性标注后的评论句子
flag = 0 # 进度条
lines = open(path, 'r').readlines()
len_lines = float(len(lines))
tagged_sentences = [] # 保存所有标注好的句子
# [ [(“'Excellent','JJ'), ('food','NN'), ('.','.')],
#   [('Superb','NNP'), ('customer','NN'), ('service','NN'), ('.','.')] ]
feature_list = [] # 挖到的feature

# 设置一个滑窗,寻找距离这个滑窗最近的一个NN、NNS
def Slip_Window_Func(tagged_sentence, i, window):
len_sentence = len(tagged_sentence)
feature = ''
k = 1

while k <= window: # 同时向目标词两边找 NN\NNS
if i-k >= 0:
if tagged_sentence[i-k][1] == ('NN' or 'NNS'):
feature = tagged_sentence[i - k][0]
if i+k < len_sentence:
if tagged_sentence[i+k][1] == ('NN' or 'NNS'):
feature = tagged_sentence[i + k][0]
if feature == '':
k += 1
continue
else:
break

return feature

# 数据预处理
flag = 0 # 进度条
print ('数据预处理进度: ')
for line in lines: # 预处理一下字符串 'Excellent/JJ food/NN ./. \n'
sentence = line[:-3].split(' ') # ['Excellent/JJ', 'food/NN', './.']
tagged_sentence = [] # 标注好的一个句子 [('Excellent','JJ'), ('food','NN'), ('.','.')]
for item in sentence:
tagged_sentence.append(item.split('/'))
tagged_sentences.append(tagged_sentence)
flag += 1
View_Bar(flag, len_lines)
# if flag == 100:
#     break
print('')

# 使用滑窗window确定 feature
flag = 0 # 进度条
print ('feature挖掘进度: ')
for tagged_sentence in tagged_sentences:
for i, tagged_word in enumerate(tagged_sentence): # ('Excellent','JJ')
if tagged_word[1] == ('JJ' or 'JJR' or 'JJS'): # 如果遇到形容词、比较级、最高级的话
feature = Slip_Window_Func(tagged_sentence, i, 5) # 设置一个滑窗,寻找距离这个滑窗最近的一个NN、NNS
if feature != '' and feature_list != []: # 如果挖到了feature的话
if feature != feature_list[-1]: # 这一步是防止挖到有滑窗交集的feature
feature_list.append(feature)
elif feature != '' and feature_list == []:
feature_list.append(feature)
else:
continue
flag += 1
View_Bar(flag, len_lines)
print ('所有的feature:')
print (feature_list)

# 将feature词汇保存一下
f = open('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/feature.txt', 'w')
for item in feature_list:
f.write(str(item) + '\n')
print('feature词汇保存完毕')

#
#  对 feature 词汇进行再清洗
#
def Feature_Data_Cleaning(path): # path是装有feature词汇的文件路径
lines = open(path, 'r').readlines()
feature_dict = {} # 保存feature的字典

# 把原始文件放到字典中
for feature in lines:
feature = feature[:-1]
if feature_dict.has_key(feature) == False: # 如果字典里没有这个feature
feature_dict[feature] = 1 # 赋一下key-value对
else: # 如果有这个feature
feature_dict[feature] += 1

# 对字典排序
feature_dict = sorted(feature_dict.iteritems(), key=lambda asd:asd[1], reverse=True) # 对value进行降序排序

print ('原始feature数目: ' + str(len(lines)))
print ('放到dict中的数目:' + str(len(feature_dict)))

# 将feature字典保存成文件
f = open('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/feature_dict.txt', 'w')
for item in feature_dict:http://write.blog.csdn.net/postedit/52794685
f.write(str(item) + '\n')

return 0 http://write.blog.csdn.net/postedit/52794685 
#   只筛选 餐厅 类型的服务行业
# filter_business_id = Filter_Business('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/yelp_academic_dataset_business.json', "Restaurants")
#   保存review
# Filter_Business_Review('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/yelp_academic_dataset_review.json', filter_business_id)

#   词性标注
# Tag_Word('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/review_text.txt')
#   筛选feature词汇
# Featuer_Word('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/word_tagged_sentences.txt', window=5)

#  对feature自会进行在清洗
Feature_Data_Cleaning('/Users/John/Desktop/yelp_dataset_challenge_academic_dataset/feature.txt')


#      17.1.3更新伪代码      #

#
# 词性标注
#
Tag_Word(path) # path是所有文件评论的路径
-读取文件并逐行放入内存中
-对每行文本:
-将文本拆分成句子列表
-对每个句子:
-对句子分词
-使用nltk词性标注处理库,对分好词的句子进行词性标注
-写入文件
-return 0

#
# 筛选feature词汇
#
Feature_Word(path, window) # path词性标注后评论好的句子, window是滑窗大小
-读取文件并逐行放入内存中
-对读取来文件进行简单的格式化预处理
-对每行文本:
-对每行文本中的每个词:
-如果这个词是形容词or形容词比较级or形容词最高级的话
-在滑窗window下同时向左、向右寻找最近的名词or名词复数,如果找到了,保存成文件
-return 0

#
# 对feature词汇进行再清洗
#
Feature_Data_Cleaning(path): # path是装有feature词汇的文件路径
-读取文件
-将所有的文件放入hash-table中,赋key-value,value是该feature出现的次数
-对hash-table降序排序
-保存成文件

为什么将文本拆分成句子列表再进行分词?

        因为nltk不是通过字典的办法知道每个词的词性,这种做法其实可行,但是一旦出现了新词就不知道是什么词性了。所以一般词性分析都是用多重隐马尔科夫链预测的,切分成句子对词性分析的准确性有好处。

为什么找feature只在一句话里找?(由逗号分隔的也算是一句话)

        我仔细观察了一下,一般在自然语言中提及到feature的大多是在一句话中,包括从句等等,这种滑窗法依旧没办法处理从句、大长句等及其复杂的句子,因为现在的自然语言处理技术还做不到让及其理解语义。所以说只能“尽可能的找”。

然是“尽可能的找”feature,精度不高做它还有什么意义啊?

        话是这么说,但是顾客在留言板下做的评论都是非常随意、白话的。像从句、大长句我用肉眼筛选了好多篇都没见到过。人们能在商家下作评论本身就是非常“给你面子”的事了,哪个顾客还认真到要用从句和长难句搞评论?笑话。如果真有这样找不到的feature,就当它是噪音,对我们后面的实验也不会有任何影响。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: