计算中非结构化数据的重复合并问题

文本大模型的在2023年开始流传一个说法,压缩及智能。这也引出来了我今天所思考的问题文本的极限压缩比应该如何实现。

数据压缩算法的核心思想是利用数据的相关性,将重复的数据信息进行压缩。常见的数据压缩算法有:

字符串匹配算法:如Lempel-Ziv-Welch(LZW)算法,通过寻找重复的子字符串,将其压缩为一个代表符号。

哈夫曼编码算法:通过将数据按照概率进行编码,将相似的数据编码为相似的二进制码,从而减少编码长度。

数字压缩算法:如Huffman编码、Run-Length Encoding(RLE)等,通过寻找数据的连续性和重复性,将其压缩为更短的表示。

数据压缩可以思考为一种背包问题。即序列长度与重复率之间存在极限空间。数据压缩确实从优化的角度去理解为一种背包问题的变形。在数据压缩中,我们的目标是用最小的空间(通常是比特或字节)来表示原始数据,同时尽可能保持数据的完整性。这个过程类似于背包问题中的"物品打包",背包容量代表了可用的存储空间限制,而数据的不同部分(如重复模式、符号频率等)可以视为背包中的不同物品,物品的价值是其信息内容,重量则是其在压缩后所需的存储空间。

在经典的背包问题中,我们需要决定哪些物品应该放入背包以最大化价值总和(在一定重量限制下)。而在数据压缩中,我们试图构建一种编码方案(如哈夫曼编码、算术编码、LZ77/LZ78等算法),使得常见或重复的数据模式可以用较短的代码表示,不常见的数据模式则用较长的代码表示,从而达到节省存储空间的目的。

然而,数据压缩的实际实现更为复杂,因为它涉及到对数据统计特性的深入理解和高效算法设计,而不只是一个简单的贪心或动态规划决策过程。尽管如此,这种比喻有助于我们理解数据压缩的核心思想,即在有限的资源约束下,寻找最优的信息表示方式。

接下来我们先用jieba分词器来做一个版本的文本数据无损压缩工具。这里面我们用token数量来表示文件压缩大小。

第一步使用read()获取文件的字符数量

python 复制代码
with open(file_path, "r", encoding="utf-8") as infile:
    b = infile.read()
len(b)

得到的结果是90768574。

那么用jieba的全分词来实现。

ini 复制代码
import ray
import jieba

# 定义一个远程任务函数
@ray.remote
def compress_text(text):
    text = jieba.lcut(text)

    return text

# 初始化Ray
ray.init()

# 读取文件并将其内容分块
file_path = '中国民用航空局txt---3.txt'
with open(file_path, "r", encoding="utf-8") as infile:
    lines = infile.readlines()

# 创建任务列表
tasks = [compress_text.remote(line) for line in lines]

# 等待所有任务完成
compressed_data = ray.get(tasks)
print(len(compressed_data))
a = 0
for one in compressed_data:
    a+=len(one)
# 最后清理资源
ray.shutdown()
a

这里我们得到的结果是分词会形成47613970个token。注释:token概念来源于自然语言处理,意味着编码数量。

那你会觉得我们到此为止了吗。不不不,我们开始引入一个新的方式。ngram进行统计后以最大频率来确定token。

首先我们要获取到连续两个字符的频次。这里的实现如下。

ini 复制代码
import ray
import jieba

# 定义一个远程任务函数
@ray.remote
def compress_text(text):
    text = jieba.lcut(text)
    a = []
    for text_one in range(len(text)-1):
        a.append(text[text_one]+text[text_one+1])
    return a

# 初始化Ray
ray.init()

# 读取文件并将其内容分块
file_path = '中国民用航空局txt---3.txt'
with open(file_path, "r", encoding="utf-8") as infile:
    lines = infile.readlines()

# 创建任务列表
tasks = [compress_text.remote(line) for line in lines]

# 等待所有任务完成
compressed_data = ray.get(tasks)
print(len(compressed_data))
a = 0
for one in compressed_data:
    a+=len(one)
# 最后清理资源
ray.shutdown()

这里理论上我们得到的a应该与前文的a大体保持一致。

47609805

接下来我们统计每个bi-gram发生的频次。

ini 复制代码
from collections import Counter

# 将所有bi-grams合并为一个大的列表
all_bigrams = [item for sublist in compressed_data for item in sublist]

# 使用Counter统计每个bi-gram出现的频次
bigram_counts = Counter(all_bigrams)

# 输出top N个最常见的bi-grams及其频次
N = 10 # 可以根据需求设置显示的前N个频次最高的bi-grams
for bigram, count in bigram_counts.most_common(N):
    print(f"{bigram}: {count}")

先看看这个数据集里面最高频的10个bi-gram是什么

makefile 复制代码
		: 1062036
(cid: 724814
cid:: 724814
)(: 469456
表格<: 405030
<form: 405030
form>: 405030
	表格: 398287
。	: 354411
	(: 157866

数据集看起来不太干净。得到的内容有点差强人意。我们能看到一个信息很重要哈。看过我之前博客的小伙伴应该知道这个

表格<: 405030 <form: 405030 form>: 405030 表格: 398287

信息是从哪里来的。是从pdf中识别到表格以后给的一种特定的拼接符号。

那么我们上一套清洗,看看清洗以后bi-gram的效果

python 复制代码
from collections import Counter

# 将所有bi-grams合并为一个大的列表
all_bigrams = [item for sublist in compressed_data for item in sublist]

# 使用Counter统计每个bi-gram出现的频次
bigram_counts = Counter(all_bigrams)
def is_chinese(s):
    """Check if the given string contains Chinese characters."""
    for char in s:
        if '\u4e00' <= char <= '\u9fff':
            return True
    return False
# 输出top N个最常见的bi-grams及其频次
N = 200 # 可以根据需求设置显示的前N个频次最高的bi-grams
for bigram, count in bigram_counts.most_common(N):
    if is_chinese(bigram):
        if "\t" in bigram:
            continue
        if "<" in bigram:
            continue
        if "," in bigram:
            continue
        if "," in bigram:
            continue
        if "。" in bigram:
            continue
        if " " in bigram:
            continue
        if ")" in bigram:
            continue
        print(f"{bigram}: {count}")

这段代码首先从collections模块导入了Counter类,该类用于方便地统计元素出现的次数。

合并所有bi-grams到一个大列表中:

ini 复制代码
all_bigrams = [item for sublist in compressed_data for item in sublist]

此处使用了Python列表推导式,将compressed_data中所有子列表的元素逐一取出,合并成一个包含所有bi-grams的大列表。

  1. 统计每个bi-gram出现的频次:通过Counter类实例化一个计数器对象,传入all_bigrams列表,它会对列表中的每一个元素进行计数,返回一个字典-like的对象,键为bi-gram,值为其出现次数。
  2. 定义了一个辅助函数is_chinese(s),用于判断给定字符串是否包含中文字符。
  3. 输出前N个最常见的bi-grams及其频次: 代码首先设置了要显示的前N个频次最高的bi-grams的数量,然后通过most_common(N)方法获取频次最高的前N个bi-gram及其频次。接着在循环体内进行一系列过滤操作,剔除了含有特殊字符(如制表符\t、小于号<、逗号,、全角逗号,、句号。、空格 、右圆括号`)的bi-gram,最后打印出满足条件的bi-gram及其频次。

规定的: 24966 航空器材: 23786 器材有限公司: 23509 科技有限公司: 22716 航空科技: 21608 通用航空: 21278 航空有限公司: 19421 的要求: 18944 运营人: 17138 要求的: 16169 001沈阳: 15572 批准的: 15402 中的: 15050 符合性: 14415 沈阳通联: 14204 通联航空: 14204 2022年: 12716 2020年: 12461 的飞行: 12438 中国民用: 11806 民用航空局: 11778 系统的: 11742 2021年: 11284 地区管理局: 10993 合格证持有人: 10805 的规定: 10641

所以接下来我们要求出频次的平均值。如果频次发生在平均值之上的。那么我们就认为这是一个值得压缩的信息。别忘记我们的目标是获取到尽可能少的token。

ini 复制代码
# 计算所有bi-gram频次之和
total_count = sum(bigram_counts.values())

# 计算bi-gram总数(即unique bi-grams的数量)
unique_bigrams_count = len(bigram_counts)

# 计算频次平均值
average_count = total_count / unique_bigrams_count if unique_bigrams_count != 0 else 0

print(f"所有bi-gram频次的平均值为: {average_count}")

计算频次的平均值。在平均值之上的考虑为压缩的连续字符空间。

所有bi-gram频次的平均值为: 9.673790635616093

ini 复制代码
def split_string_with_bigrams(original_text, bigram_list):
    # 假设 bigram_list 已经包含了按顺序排列的所有bi-gram
    split_text = []
    current_index = 0
    cut_text = jieba.lcut(original_text)
    for word_index in range(len(cut_text)-1):
        # print(cut_text[word_index] + cut_text[word_index+1])

        if cut_text[word_index] + cut_text[word_index+1] in bigram_list:
            print(cut_text[word_index] + cut_text[word_index+1])
            split_text.append(cut_text[word_index] + cut_text[word_index+1])
            word_index += 1
        else: 
            split_text.append(cut_text[word_index])
    split_text.append(cut_text[-1])
    return split_text

# 假设有如下已分析好的bigram列表
# bigram_list = ['你好世界', '世界欢迎', '欢迎来到', ...]

# 原始文本
original_text = "你好世界,世界欢迎你来到这里"

# 应用函数进行切分
result = split_string_with_bigrams(original_text, c)
print(result)
 

接下来我们统计一下token数量 还是用ray框架实现多进程加速

ini 复制代码
import ray
import jieba
ray.shutdown()
# 定义一个远程任务函数
@ray.remote
def split_string_with_bigrams(original_text, bigram_list):
    # 假设 bigram_list 已经包含了按顺序排列的所有bi-gram
    cut_text = jieba.lcut(original_text)
    split_text = []
    current_index = 0
    for word_index in range(len(cut_text) - 1):
        if cut_text[word_index] + cut_text[word_index + 1] in bigram_list:
            split_text.append(cut_text[word_index] + cut_text[word_index + 1])
            word_index += 1
        else:
            split_text.append(cut_text[word_index])
    if cut_text[-1] not in split_text[-1]:
        split_text.append(cut_text[-1])
    return split_text

# 初始化Ray
ray.init()
# 读取文件并将其内容分块
file_path = '中国民用航空局txt---3.txt'
with open(file_path, "r", encoding="utf-8") as infile:
    lines = infile.readlines()

# 创建并提交任务列表
task_results = [split_string_with_bigrams.remote(line, c) for line in lines]

# 并行等待所有任务完成
results = ray.get(task_results)

# 处理所有行的结果
token_count = 0
for result in results:
    token_count+=len(result)

# 最后清理资源
ray.shutdown()

得到结果

47613969

这里我们提一个题外话,深度学习的矩阵计算问题中存在大量的冗余无意义的计算。这个理论基础是目前deepspeed等分布式并行深度计算框架所利用到的计算加速理论之一。

在深度学习模型的训练过程中,尤其是在大规模的神经网络中,存在着大量的冗余计算。这些冗余体现在以下几个方面:

  1. 重复计算:在反向传播过程中,每一层都会计算其输入的梯度,而这些梯度往往会被下一层反复使用。例如,同一层在多次迭代中的梯度会有一部分相同,而在标准的训练流程中每次迭代都重新计算。
  2. 稀疏性利用:许多实际问题中,输入数据或权重矩阵可能存在稀疏性,即大量元素为零。但在传统的矩阵乘法中,即使是零元素也会参与计算,造成不必要的计算开销。
  3. 通信代价:在分布式计算环境下,为了保持模型的一致性,各计算节点间需要频繁同步梯度信息,而这部分通信过程中的数据传输也可能包含大量冗余。

为解决这些问题,现代深度学习框架和专门针对加速计算的库(如DeepSpeed)采用了各种优化策略:

  • 动态计算图与内存优化:动态调整计算图,只计算必要的梯度,避免不必要的中间结果存储和计算。
  • 自动微分库中的稀疏梯度支持:利用稀疏矩阵运算,减少不必要的计算,特别在大模型和自然语言处理领域中有广泛应用。
  • 梯度累积与检查点重计算:在多个小批次上累积梯度再进行更新,减少通信次数;在某些情况下,使用检查点重计算而非梯度传播,避免在较深的网络层级中传递梯度。
  • 模型并行、数据并行与混合并行:通过分割模型或数据在多个GPU或计算节点上并行处理,同时结合高效的通信压缩和减少冗余同步技术。
  • 计算图优化技术:例如张量分解、层融合、循环展开等技术减少计算量和内存占用。

通过上述优化手段,深度学习计算框架能够有效地减少冗余计算,提高训练效率,并适应更大规模的模型训练需求。

最后我想说,我并不认为用生成式模型来辅助创作是一个不好的事情,反而这可以提供给创作者一个有效的思维路径。因为收到了平台的警告。

相关推荐
yuanbenshidiaos1 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习1 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA1 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo1 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc1 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
游是水里的游3 小时前
【算法day20】回溯:子集与全排列问题
算法
yoyobravery3 小时前
c语言大一期末复习
c语言·开发语言·算法
Jiude3 小时前
算法题题解记录——双变量问题的 “枚举右,维护左”
python·算法·面试
被AI抢饭碗的人3 小时前
算法题(13):异或变换
算法
nuyoah♂4 小时前
DAY36|动态规划Part04|LeetCode:1049. 最后一块石头的重量 II、494. 目标和、474.一和零
算法·leetcode·动态规划