建模杂谈系列93 增量TF-IDF

说明

简单就是美

说起来这个项目很早之前做过,最近用到,再梳理一次。

这篇文章草稿是在2021年的,现在是2024年,继续写完它。

内容

1 TF-IDF

来自百度的解释:TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

概念:

  • 1 语料 corpus: 或者说文件集
  • 2 文档 document: 或者说一篇文章
  • 3 词频 TF(Term Frequency):TF表示词条在文档d中出现的频率。
  • 4 逆词频IDF(Inverse Document Frequency):如果包含词条t的文档越少,也就是n越小,IDF越大,则说明词条t具有很好的类别区分能力。

关于TF-IDF,知乎的这篇文章还不错

当有TF(词频)和IDF(逆文档频率)后,将这两个词相乘,就能得到一个词的TF-IDF的值。某个词在文章中的TF-IDF越大,那么一般而言这个词在这篇文章的重要性会越高,所以通过计算文章中各个词的TF-IDF,由大到小排序,排在最前面的几个词,就是该文章的关键词。

在这里插入图片描述

一些拓展是word2vec, 很早就有打算看,但现在仍然没时间看(不过原来曾经用SAS搞过一下n-grams)。也mark一下吧:

word2vec是浅层神经网络模型,有两种网络结构,分别为CBOW和skip-gram。word2vec是浅层神经网络模型,有两种网络结构,分别为CBOW和skip-gram。

# 旧文档
doc1 = list('ambulancewithpolicecars')
doc2 = list('applebananacherry')
doc3 = list('benzaudivolkswagan')
corpus = [doc1, doc2, doc3]
c_list1 = [1,2,3]


array([list(['a', 'm', 'b', 'u', 'l', 'a', 'n', 'c', 'e', 'w', 'i', 't', 'h', 'p', 'o', 'l', 'i', 'c', 'e', 'c', 'a', 'r', 's']),
       list(['a', 'p', 'p', 'l', 'e', 'b', 'a', 'n', 'a', 'n', 'a', 'c', 'h', 'e', 'r', 'r', 'y']),
       list(['b', 'e', 'n', 'z', 'a', 'u', 'd', 'i', 'v', 'o', 'l', 'k', 's', 'w', 'a', 'g', 'a', 'n'])]
  
# 新文档
new_doc1 = list('hpdelllenovo')
new_doc2 = list('friendslove')
new_corpus = [new_doc1, new_doc2]
c_list2 = [1, 2]

# 原始idf主字典
main_idf_dict = {}

from collections import Counter
from itertools import chain
import pandas as pd
import numpy as np
import copy

corpus_s = pd.Series(corpus)
# TF calculation
corpus_s1 = corpus_s.apply(lambda x: dict(Counter(x)))
doc_words = corpus_s.apply(len)

In [11]: corpus_s1
Out[11]:
0    {'a': 3, 'm': 1, 'b': 1, 'u': 1, 'l': 2, 'n': ...
1    {'a': 4, 'p': 2, 'l': 1, 'e': 2, 'b': 1, 'n': ...
2    {'b': 1, 'e': 1, 'n': 2, 'z': 1, 'a': 3, 'u': ...
In [10]: doc_words
Out[10]:
0    23
1    17
2    18
dtype: int64


# IDF calculation
corpus_s2 = corpus_s1.apply(lambda x: list(x.keys()))
idf_dict = Counter(chain.from_iterable(corpus_s2))

In [13]: idf_dict
Out[13]:
Counter({'a': 3,
         'm': 1,
         'b': 3,
         'u': 2,
         'l': 3,
         'n': 3,
         'c': 2,
         'e': 3,
         'w': 2,
         'i': 2,
         't': 1,
         'h': 2,
         'p': 2,
         'o': 2,
         'r': 2,
         's': 2,
         'y': 1,
         'z': 1,
         'd': 1,
         'v': 1,
         'k': 1,
         'g': 1})
# 原始idf主字典
main_idf_dict = {}

s1 = pd.Series(main_idf_dict)
s2 = pd.Series(idf_dict)
s3 = s1+s2
add_key = dict(s3.dropna())
main_idf_dict.update(idf_dict)
main_idf_dict.update(add_key)
mod_keys = list(idf_dict.keys()) # 可以并行


In [16]: main_idf_dict
Out[16]:
{'a': 3,
 'm': 1,
 'b': 3,
 'u': 2,
 'l': 3,
 'n': 3,
 'c': 2,
 'e': 3,
 'w': 2,
 'i': 2,
 't': 1,
 'h': 2,
 'p': 2,
 'o': 2,
 'r': 2,
 's': 2,
 'y': 1,
 'z': 1,
 'd': 1,
 'v': 1,
 'k': 1,
 'g': 1}

In [17]: mod_keys
Out[17]:
['a',
 'm',
 'b',
 'u',
 'l',
 'n',
 'c',
 'e',
 'w',
 'i',
 't',
 'h',
 'p',
 'o',
 'r',
 's',
 'y',
 'z',
 'd',
 'v',
 'k',
 'g']
  • 1 首先有新老的文档集 list of character list
  • 2 将文档转为Series,这样后续的计算就是by 文档进行并行计算的
  • 3 然后就可以用apply方法并行的计算词频(TF)和逆文档频率(IDF),现在注意力放在IDF上
  • 4 corpus_s1已经获得了文档的词频统计,corpus_s2则取corpus_s1字典的键作为统计元素,这样无论某个词出现了几次,当作为键被统计时,只是1次
  • 5 通过对list of set 的Counter X iterable, 实现了快速的频数统计(几乎也是并行的)
  • 6 此时对main_idf_dict(最初为空)操作,变为Series,最初的s1是空序列
  • 7 然后将此时的新增idf_dict进行转序列,变为s2
  • 8 s1+s2, 由于一个列表为空,加出的s3也全部为空,因为没有相同的对齐索引
  • 9 如果有相同的元素,那么add_key 就是相加后的结果(s3.dropna()剩下的必然是相同,被相加的元素)
  • 10 接下来的main_idf_dict.update(idf_dict)会将当前的字典覆盖上,但是那些相加的部分也被覆盖掉了
  • 11 然后再用main_idf_dict.update(add_key)将相加的部分覆盖掉,此时就是正常的结果了
  • 12 最后mod_keys是本次更改涉及的所有键。

总计上这么写也是ok的,虽然有些无效操作(覆盖2次)。毕竟这个代码写在好几年前,如果考虑到数据库的话,其实可以根据每次更新的idf_dict中的key进行数据查找,更改好之后直接update回去就可以了。现在这种写法更偏向离线操作和持久化较多的情况。

上面操作后,初始的main_idf_dict产生了变化

In [23]: main_idf_dict
Out[23]:
{'a': 3,
 'm': 1,
 'b': 3,
 'u': 2,
 'l': 3,
 'n': 3,
 'c': 2,
 'e': 3,
 'w': 2,
 'i': 2,
 't': 1,
 'h': 2,
 'p': 2,
 'o': 2,
 'r': 2,
 's': 2,
 'y': 1,
 'z': 1,
 'd': 1,
 'v': 1,
 'k': 1,
 'g': 1}

2 增量IDF

TF-IDF的实现并不难,但是当面对特别巨大的语料,或者需要持续不断的更新时,就需要改变一下实现的方式了。当算法可以从内存搬到硬盘,允许增量的计算时,还是很有意思的

值得注意的是,TF-IDF应该针对的是较长的文本端,主题内容能够通过分词体现出来。一篇文章的TF总是容易计算,关键还是IDF的增量计算。

接着上面的代码继续,假设这时候来了新的文档(new_corpus),同样执行计算,此时可以封装一下

import pandas as pd
import numpy as np
from collections import Counter
from itertools import chain

def get_idf_dict(corpus = None):
	corpus_s = pd.Series(corpus)
	# TF calculation
	corpus_s1 = corpus_s.apply(lambda x: dict(Counter(x)))
	doc_words = corpus_s.apply(len)
	# IDF calculation
	corpus_s2 = corpus_s1.apply(lambda x: list(x.keys()))
	idf_dict = Counter(chain.from_iterable(corpus_s2))
	return idf_dict
new_idf_dict = get_idf_dict(new_corpus)
Counter({'h': 1,
         'p': 1,
         'd': 2,
         'e': 2,
         'l': 2,
         'n': 2,
         'o': 2,
         'v': 2,
         'f': 1,
         'r': 1,
         'i': 1,
         's': 1})

然后将这部分增量的idf叠加上

import pandas as pd 

def increadd_dict(master_dict = None, slave_dict = None):
    s1 = pd.Series(master_dict)
    s2 = pd.Series(slave_dict)
    s3 = s1+s2
    add_key = dict(s3.dropna())
    master_dict.update(slave_dict)
    master_dict.update(add_key)
    mod_keys = list(s2.keys()) # 可以并行
    return master_dict, mod_keys

在这里插入图片描述
所以更新后的字典

main_idf_dict, mod_keys = increadd_dict(main_idf_dict,new_idf_dict)
改变的键值:['h', 'p', 'd', 'e', 'l', 'n', 'o', 'v', 'f', 'r', 'i', 's']
{'a': 3,
 'm': 1,
 'b': 3,
 'u': 2,
 'l': 5.0,
 'n': 5.0,
 'c': 2,
 'e': 5.0,
 'w': 2,
 'i': 3.0,
 't': 1,
 'h': 3.0,
 'p': 3.0,
 'o': 4.0,
 'r': 3.0,
 's': 3.0,
 'y': 1,
 'z': 1,
 'd': 3.0,
 'v': 3.0,
 'k': 1,
 'g': 1,
 'f': 1}

以上实现了增加新文档时,IDF词频的统计,是最麻烦的部分。

3 完整的IDF

IDF是学习的过程

正如机器学习分为train和predict两部分一样,统计idf的过程本身是一种学习过程。每次接纳新的数据,模型了解的统计分布应当是越全面的。但是为了避免简单的重复,我们必须要记下学过哪些文档。同时,IDF恰好也需要所有的文档数作为分子,一举两得。

我们假定,corpus里的每一行,也就是每一篇文章,会有一个唯一的id。这个id可以是人为的编号,也可以是内容的md5 id。

每一次的学习,对应于一次语料的增量的学习。每一个idf模型,是由其训练的语料集合唯一确定的。为统一期间,我们把corpus较为dataset,这样就可以和一般的规范接上了。

这个简单的模型由 dataset, pid_list(文章编号)唯一决定。当进行增量更新时,得到的结果是新模型。当然新模型是一部分新数据在老数据上叠加的结果,所以是增量。很像大模型的微调

所以在每一次训练时,需要传入的是 zip(pid_list, data_list),如果从离线状态来理解,那么idf模型会保留一个pid_set和idf_dict。其中pid_set既提供了文档总数,同时能过滤掉重复的数据。

初始化状态

idf_model = {'pid_set': set([]), 'idf_dict' : {}}

def idf_train(idf_model = None, data_list = None, pid_list = None):
	pid_set = idf_model['pid_set']
	idf_dict = idf_model['idf_dict']
	gap_set = set(pid_list) - set(pid_set)
	if len(gap_set):
		print('updating %s recs ' % len(gap_set))
	else:
		print('No UPDATING')
	filter_data_list = []
	for i, v in enumerate(pid_list):
		if v in gap_set:
			filter_data_list.append(data_list[i])
	new_idf_dict = get_idf_dict(filter_data_list)
	_idf_dict,mod_keys = increadd_dict(idf_dict, new_idf_dict)
	idf_model['idf_dict'] = _idf_dict
	idf_model['pid_set'] = pid_set | gap_set
	return idf_model

在这里插入图片描述

可以看到,第二次训练没有更新,因为新的pid_list = [1,2]是和已有的id重复的。

idf_model1 = idf_train(idf_model = idf_model, data_list= corpus, pid_list= c_list1)
updating 3 recs
idf_model2 = idf_train(idf_model = idf_model, data_list= new_corpus, pid_list= c_list2)
No UPDATING

4 小结

写到这里差不多了,解决了最核心的问题(计算增量idf)。词频随着新数据而更新,而文档总数在锁定idf_model,计算集合长度就可以了。

就TFIDF本身的计算而言,还有TF的归一化计算(实际上在计算IDF时,这部分功能已经实现了)。

Next:

  • 1 继续完成整个TF-IDF的功能
  • 2 将TF-IDF进行对象化,在idf_model函数时已经看出来,应该对象化。
  • 3 将训练过程规范化,每次的训练数据按规范入库,并锁定每次增量训练的dataset和模型)
  • 4 将结果封装为服务,随时可以调用不同的idf模型
  • 5 完成前端的展示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/488169.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网络安全笔记-day8,DHCP部署

DHCP部署与安全 全称(Dynamic Host Configura Protocol)动态主机配置协议 DHCP原理 DHCP协议_科来测试dhcp网络包-CSDN博客🔍 注意的是利用广播地址发送包 ACK(确认) 如果DHCP服务器损坏,则在87.5%时…

Python Flask框架 -- flask-migrate迁移ORM模型

# 之前使用的这个db.create_all()很有局限性,它不能把在class里修改的东西同步上数据库,所以不用了 # with app.app_context(): # 请求应用上下文 # db.create_all() # 把所有的表同步到数据库中去 例如,在User类中增加一个email字段&…

2.6 IDE(集成开发环境)是什么

IDE(集成开发环境)是什么 IDE 是 Integrated Development Environment 的缩写,中文称为集成开发环境,用来表示辅助程序员开发的应用软件,是它们的一个总称。 通过前面章节的学习我们知道,运行 C 语言&…

各大pdf转word软件都用的哪家的ocr引擎?

国内一般的PDF软件一般都调用某国际PDF原厂的OCR接口,但这家公司是主要做PDF,在OCR方面并不专注,一些不是很复杂的场景还能应付得过来,复杂一点的效果就强差人意了,推荐用金鸣表格文字识别系统,它主要有以下…

基于CNN-RNN的动态手势识别系统实现与解析

一、环境配置 为了成功实现基于CNN-RNN的动态手势识别系统,你需要确保你的开发环境已经安装了以下必要的库和工具: Python:推荐使用Python 3.x版本,作为主要的编程语言。TensorFlow:深度学习框架,用于构建…

ElementPlus Upload组件使用compressorjs压缩图片上传

需求 Compressor.js 是一个用于在客户端(即在浏览器中)对图片进行压缩的 JavaScript 库。使用它有以下几个优点和意义: 减少文件大小: 图片通常是网页中占用大量带宽的资源之一。通过使用 Compressor.js 对图片进行压缩,可以显著…

力扣面试150 直线上最多的点数 数学 直线斜率 欧几里得求最大公约数

Problem: 149. 直线上最多的点数 思路 👨‍🏫 参考题解 💖 枚举直线 枚举统计 时间复杂度: O ( n 3 ) O(n^3) O(n3) 空间复杂度: O ( 1 ) O(1) O(1) class Solution {public int maxPoints(int[][] points){int n points.length;int…

数据库之备份与恢复

MySQL完全备份与恢复 数据备份的重要性 在生产环境中,数据的安全性至关重要任何数据的丢失都可能产生严重的后果造成数据丢失的原因 程序错误 人为操作错误 运算错误 磁盘故障 灾难(如火灾、地震)和盗窃 数据库备份的分类 物理角度 物理备份:对数据库操作系统的…

【网络爬虫】(1) 网络请求,urllib库介绍

各位同学好,今天开始和各位分享一下python网络爬虫技巧,从基本的函数开始,到项目实战。那我们开始吧。 1. 基本概念 这里简单介绍一下后续学习中需要掌握的概念。 (1)http 和 https 协议。http是超文本传输&#xf…

Visual Studio项目编译和运行依赖第三方库的项目

1.创建项目,这里创建的项目是依赖于.sln的项目,非CMake项目 2.添加第三方库依赖的头文件和库文件路劲 3.添加第三方依赖库文件 4.项目配置有2个,一个是Debug,一个是Release,如果你只配置了Debug,编译和运行…

厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能原理篇

接着上一篇《厨余垃圾处理设备工业监控PLC连接APP小程序智能软硬件开发之功能结构篇》继续总结一下厨余垃圾处理设备智能软硬件统的原理。所有的软硬件系统全是自己一人独自开发,看法和角度难免有局限性。希望抛砖引玉,将该智能软硬件系统分享给更多有类…

java的封装

封装概述 java中的封装指的是将一系列有关的事物的共同属性和行为提取出来放到一个类中,隐藏对象的实行和现实细节,仅对外提供公共的访问方式的操作。这样说起来感觉很抽象,也不好理解,这里不妨举一个例子。将配置电脑这个动作看成…

伪装目标检测之注意力CBAM:《Convolutional Block Attention Module》

论文地址:link 代码:link 摘要 我们提出了卷积块注意力模块(CBAM),这是一种简单而有效的用于前馈卷积神经网络的注意力模块。给定一个中间特征图,我们的模块依次推断沿着两个独立维度的注意力图&#xff…

Qt实现简易的多线程TCP服务器(支持多个客户端连接)附源码

目录 一.UI界面的设计 二.服务器的启动 三.实现自定义的TcpServer类 1.在widget中声明自定义TcpServer类的成员变量 2.在TcpServer的构造函数中对于我们声明的m_widget进行初始化,m_widget我们用于后续的显示消息等,说白了就是主界面的更新显示等 …

为何ChatGPT日耗电超50万度?

看新闻说,ChatGPT每天的耗电量是50万度,国内每个家庭日均的耗电量不到10度,ChatGPT耗电相当于国内5万个家庭用量。 网上流传,英伟达创始人黄仁勋说:“AI的尽头是光伏和储能”,大佬的眼光就是毒辣&#xff…

使用LLaVA模型实现以文搜图和以图搜图

本文将会详细介绍如何使用多模态模型——LLaVA模型来实现以文搜图和以图搜图的功能。本文仅为示例Demo,并不能代表实际的以文搜图和以图搜图的技术实现方案。 1、实现原理 使用多模态模型获取图片的标题和详细描述以文搜图功能:使用ES实现查询匹配&…

深入了解 Linux 中的 MTD 设备:/dev/mtd* 与 /dev/mtdblock*

目录 前言一、什么是MTD子系统?二、 /dev/mtd* 设备文件用途注意事项 三、/dev/mtdblock* 设备文件用途注意事项 三、这两种设备文件的关系四、关norflash的一些小知识 前言 在嵌入式Linux系统的世界里,非易失性存储技术扮演着至关重要的角色。MTD&#…

面试知识汇总——垃圾回收器(分代收集算法)

分代收集算法 根据对象的存活周期,把内存分成多个区域,不同区域使用不同的回收算法回收对象。 对象在创建的时候,会先存放到伊甸园。当伊甸园满了之后,就会触发垃圾回收。 这个回收的过程是:把伊甸园中的对象拷贝到F…

初识redis(一)

前言 引用的是这本书的原话 Redis[1]是一种基于键值对(key-value)的NoSQL数据库,与很多键值对数据库不同的是,Redis中的值可以是由string(字符串)、hash(哈希)、list(列…

Android15功能和 API 概览

Android 15 面向开发者引入了一些出色的新功能和 API。以下部分总结了这些功能,以帮助您开始使用相关 API。 如需查看新增、修改和移除的 API 的详细列表,请参阅 API 差异报告。如需详细了解新的 API,请访问 Android API 参考文档&#xff0…