文本大模型的在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的大列表。
- 统计每个bi-gram出现的频次:通过Counter类实例化一个计数器对象,传入all_bigrams列表,它会对列表中的每一个元素进行计数,返回一个字典-like的对象,键为bi-gram,值为其出现次数。
- 定义了一个辅助函数is_chinese(s),用于判断给定字符串是否包含中文字符。
- 输出前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等分布式并行深度计算框架所利用到的计算加速理论之一。
在深度学习模型的训练过程中,尤其是在大规模的神经网络中,存在着大量的冗余计算。这些冗余体现在以下几个方面:
- 重复计算:在反向传播过程中,每一层都会计算其输入的梯度,而这些梯度往往会被下一层反复使用。例如,同一层在多次迭代中的梯度会有一部分相同,而在标准的训练流程中每次迭代都重新计算。
- 稀疏性利用:许多实际问题中,输入数据或权重矩阵可能存在稀疏性,即大量元素为零。但在传统的矩阵乘法中,即使是零元素也会参与计算,造成不必要的计算开销。
- 通信代价:在分布式计算环境下,为了保持模型的一致性,各计算节点间需要频繁同步梯度信息,而这部分通信过程中的数据传输也可能包含大量冗余。
为解决这些问题,现代深度学习框架和专门针对加速计算的库(如DeepSpeed)采用了各种优化策略:
- 动态计算图与内存优化:动态调整计算图,只计算必要的梯度,避免不必要的中间结果存储和计算。
- 自动微分库中的稀疏梯度支持:利用稀疏矩阵运算,减少不必要的计算,特别在大模型和自然语言处理领域中有广泛应用。
- 梯度累积与检查点重计算:在多个小批次上累积梯度再进行更新,减少通信次数;在某些情况下,使用检查点重计算而非梯度传播,避免在较深的网络层级中传递梯度。
- 模型并行、数据并行与混合并行:通过分割模型或数据在多个GPU或计算节点上并行处理,同时结合高效的通信压缩和减少冗余同步技术。
- 计算图优化技术:例如张量分解、层融合、循环展开等技术减少计算量和内存占用。
通过上述优化手段,深度学习计算框架能够有效地减少冗余计算,提高训练效率,并适应更大规模的模型训练需求。
最后我想说,我并不认为用生成式模型来辅助创作是一个不好的事情,反而这可以提供给创作者一个有效的思维路径。因为收到了平台的警告。