短视频关键词提取:基于TF-IDF和LDA

最近在做一个短视频的APP,需要对每条短视频提取特征标签。因为短视频数量比较多,更新也比较快,人工逐条打标签显然是不太现实的。开发同学都没有标签建设的经验,而且人手有限,前期主要精力都在服务端底层结构实现和接口实现上。于是,产品狗只能自己做一些调研和试验了orz

经过一番调研,前期我打算简单点处理,主要从以下两个角度试验一下:

  1. 使用一些新闻资讯行业常见的关键词提取算法,如TF-IDF,fastText等,对短视频标题和介绍进行关键词提取,把提取出来的关键词作为短视频的特征标签。
  2. 使用一些聚类算法,如LDA等,将短视频进行聚类,然后人工查看每个类目下的内容并根据内容补充类目名称。主要用于补充一些在标题字面里没有出现的特征(类目)。

这些都是nlp领域比较常见的算法,不需要重新造轮子。针对方案1,我们使用jieba分词内置的关键词提取方法来实现;针对方案2,我们使用谷歌的word2vec来实现。

TF-IDF提取关键词

TF-IDF算法的原理比较简单,主要是基于这样的思想:那些在当前query里出现频率较高,同时又在其他query里出现较少的词,更可能反映当前query的特征。

jieba分词内置了一个extract_tags方法,使用的就是TF-IDF算法。它还内置了一个IDF字典,这样就不用自己用大量语料来统计词频,我们准备拿来直接用了。不过这里需要留个疑问,因为jieba内置的IDF字典应该是基于更广义上的语料计算出来的,如果我们只用短视频标题介绍来做语料计算IDF,效果会更好还是更差呢?有时间了验证下,目前先直接用内置的IDF字典试下吧。

jieba的extract_tags用法:

jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())  

其中:

  • sentence 为待提取的文本
  • topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20
  • withWeight 为是否一并返回关键词权重值,默认值为 False
  • allowPOS 仅包括指定词性的词,默认值为空,即不筛选

我们这里只取名词类词性,并截取权重最高的前5个。 这里可以看到全部词性对照表

def get_tags(doc):  
    tags = jieba.analyse.extract_tags(doc, topK = 5, withWeight = False, allowPOS = ('n','nr','ns','nt','nz','vn'))
    return doc + '\t' + ','.join(tags)

所有的短视频标题保存在titles.txt文件,每行一个标题。为了加快提取速度,我们这里使用python的multiprocessing包来实现多进程提取。

整个titles.txt文件读入太消耗内存,我们首先实现一个任务生成器,每次从文件中读取一条标题并返回:

class LineGenerator(object):  
    def __init__(self, path):
        self.path = path

    def __iter__(self):
        for line in open(self.path, 'r'):
            yield line.strip()

好了,现在来实现多进程分词:

p = multiprocessing.Pool(multiprocessing.cpu_count())  
f_in = "titles.txt"  
tasks = LineGenerator(f_in)  
results = p.map(get_tags, tasks)  
with open("keywords.txt", 'w') as f_out:  
    for line in results:
        f_out.write(line)
        f_out.write('\n')

运行一下,发现内存占用还是很快飙了上来。看起来像整个titles.txt文件全被加载到了内存里。 看了下multiprocessing的官方文档,原来问题在我使用的map函数上。 它会把所有的任务读入任务池,供各个进程取取用,实际上也就相当于把整个titles.txt文件读入了内存。

改用imap即可解决这个问题:

imap(func, iterable[, chunksize])

An equivalent of itertools.imap().

The chunksize argument is the same as the one used by the map() method. For very long iterables using a large value for chunksize can make the job complete much faster than using the default value of 1.

Also if chunksize is 1 then the next() method of the iterator returned by the imap() method has an optional timeout parameter: next(timeout) will raise multiprocessing.TimeoutError if the result cannot be returned within timeout seconds.

另外根据文档里的说法,我们设置一下chunksize参数。 修改后的代码如下:

p = multiprocessing.Pool(multiprocessing.cpu_count())  
f_in = "titles.txt"  
tasks = LineGenerator(f_in)  
results = p.imap(get_tags, tasks, chunksize=200)  
with open("keywords.txt", 'w') as f_out:  
    for line in results:
        f_out.write(line)
        f_out.write('\n')

至此我们实现了多进程的关键词提取。完整代码如下:

#-*- coding:utf-8 -*-

import multiprocessing  
import jieba  
import jieba.analyse  
import sys  
import time

reload(sys)  
sys.setdefaultencoding('utf8')

class LineGenerator(object):  
    def __init__(self, path):
        self.path = path

    def __iter__(self):
        for line in open(self.path, 'r'):
            yield line.strip()

def get_tags(doc):  
    tags = jieba.analyse.extract_tags(doc, topK = 5, withWeight = False, allowPOS = ('n','nr','ns','nt','nz','vn'))
    return doc + '\t' + ','.join(tags)


if __name__ == "__main__":  
    start = time.time()
    p = multiprocessing.Pool(multiprocessing.cpu_count())
    f_in = "titles.txt"
    tasks = LineGenerator(f_in)
    results = p.imap(get_tags, tasks, chunksize=200)
    with open("keywords.txt", 'w') as f_out:
        for line in results:
            f_out.write(line)
            f_out.write('\n')
    end = time.time()
    print end - start

使用Word2vec实现LDA聚类

待补充

Derek

Ask basic questions.