《多媒体数据处理》上机实验报告
一、第一次实验:算术编码
1.算法描述
算术编码是一种无损压缩算法,其主要功能是将输入的符号序列转换为一个比特流,以减小数据的大小。通过不断缩小的区间范围,算术编码可以实现高效的数据压缩。需要注意的是,解码时需要使用相同的符号表和频率表来还原原始数据。
(1)例如:
-----------------------------------------------------------------------------------------
for(int i=0;i<count;i++){
flag=0;
lenth=high-low;
for(int j=0;j<n;j++){
if(str[i]==num[j]){
flag=1;
high=low+lenth*highedge(j,chance);
low=low+lenth*lowedge(j,chance);
}
}
//判断是否含有非字典元素
if(flag==0){
printf("字符串含有非字典元素");
break;
}
}
-----------------------------------------------------------------------------------------
功能:确定编码区间;
(2)例如:
-----------------------------------------------------------------------------------------
while(1){
sum=sum+b;
if(sum<low){
code2[m]=1;
}
if(sum>high){
sum=sum-b;
code2[m]=0;
}
if((sum>low)&&(sum<high)){
code2[m]=1;
break;
}
b=(double)b/2;
m++;
}
//还原二进制代码并输出编码值和解码值
for(int i=0;i<=m;i++){
printf("%d",code2[i]);
}
putchar('\n');
printf("%.15Lf",sum);
-----------------------------------------------------------------------------------------
功能:根据区间进行解码并还原二进制代码并输出编码值和解码值。
算法流程:
输入:字符字典和对应概率、需要编码的字符串和解码的位数;
输出:编码、解码字符串;
Step 1:初始化:确定符号表、频率表和累积概率区间;
Step 2:设定区间:根据输入的符号序列,在累积概率区间中为每个符号确定对应的区间范围;
Step 3:编码:将每个符号对应的区间映射为一个子区间,并将这些子区间连接起来形成一个新的区间;
Step 4:收缩区间:根据每个符号对应的子区间,不断缩小整体区间的范围;
Step 5:输出编码:将缩小后的区间转换为比特流输出;
Step 6:反向解码:按照编码加和得解码值确定区间。
2.核心代码
//确定编码区间
for(int i=0;i<count;i++){
flag=0;
lenth=high-low;
for(int j=0;j<n;j++){
if(str[i]==num[j]){
flag=1;
high=low+lenth*highedge(j,chance);
low=low+lenth*lowedge(j,chance);
}
}
//判断是否含有非字典元素
if(flag==0){
printf("字符串含有非字典元素!\n编码结束!");
return 0;
}
}
printf("编码开始:\n");
//输出编码区间
printf("编码区间为:%.16Lf ~ %.16Lf\n",low,high);
//根据区间进行解码
while(1){
sum=sum+b;
if(sum<low){
code2[m]=1;
}
if(sum>high){
sum=sum-b;
code2[m]=0;
}
if((sum>low)&&(sum<high)){
code2[m]=1;
break;
}
b=(double)b/2;
m++;
}
//还原二进制代码并输出编码值和编码
printf("字符串编码为:");
for(int i=0;i<=m;i++){
printf("%d",code2[i]);
}
putchar('\n');
printf("字符串编码为:%.16Lf\n",sum);
printf("编码结束!\n");
3.实验结果
测试数据,输入、输出的展示,对输出结果的分析。
4.亮点、分析与总结
对算法功能、算法改进、关键步骤、验收时的错误等任何方面内容的分析与总结。
算法功能分为编码和解码,编码需要先算出编码数值然后for循环找到编码区间以及编码。解码需要按照给以的编码反向for循环让编码值在区间中寻找对应字符并逐渐确定编码字符。
过程中思考是否只需要给出编码就一定可以解码到原来我们想要的字符串。实际上不是,我们还需要限定位数,因为只要在编码区间内不是一个确定的值就可以无限延伸小数位,所以一定要限制位数才能得到我们想要的值。
实验让我更深入地理解了算术编码的理论基础。通过实际实现编码器和解码器,我能够直观地感受到编码区间的动态调整过程,以及如何根据符号的概率分布来生成压缩后的数据流。算术编码实验不仅帮助我深入理解了这一压缩算法的原理和实现细节,还使我能够更好地掌握其在数据压缩中的应用技巧和优化策略。这种理论与实践相结合的学习方式,为我今后在数据压缩和信息理论领域的研究和应用奠定了坚实的基础。
二、第二次实验:PCA算法
1. 算法描述
PCA(Principal Component Analysis,主成分分析)是一种常用的数据降维技术,其主要功能是通过线性变换将数据投影到一个新的坐标系中,从而找到数据中的主要成分或特征。这些主要成分被称为主成分,其具有最大的方差,因此保留了数据中的大部分信息。
主要功能:
-数据降维:PCA通过将数据投影到较低维度的空间中,实现数据的降维。这有助于减少数据集的维度,减少存储空间和计算复杂度,同时能够去除数据中的噪声和冗余信息。
-特征提取:PCA能够从原始数据中提取出最能代表数据变化的主成分。这些主成分是数据中方差最大的方向,通常反映了数据的重要特征。
-数据可视化:通过PCA,可以将高维数据映射到二维或三维空间中,便于数据的可视化展示和分析,帮助理解数据的内在结构和关系
(1)例如:
-----------------------------------------------------------------------------------------
C_before = (1 / (X.shape[1] - 1)) * np.dot(X, X.T)
# 计算特征值和对应的特征向量矩阵
eigenvalues, eigenvectors = np.linalg.eig(C_before)
# 求出最大的k个特征值的索引,np.argsort返回特征值从小到大排序后的数在原数组中的索引
indexes = np.argsort(eigenvalues)[::-1][:k] # [::-1]将数组倒序,[:k]取前k个值
# 选择特征向量矩阵中列索引在indexes中的所有列构成最终的投影矩阵
P = eigenvectors[:, indexes]
# 将特征向量转为沿行方向排列,P:5x32
P = P.T
# 计算降维结果Y=PX(P:5x32,X:32x68040)
-----------------------------------------------------------------------------------------
功能:降低数据维度;
算法流程:
输入:数据集 ColorHistogram.asc,降维数 k;
输出:原始数据的协方差矩阵、PCA降维后的数据、降维后数据的协方差矩阵;
Step 1:数据标准化:首先,对原始数据进行标准化处理,使得每个特征的均值为0,方差为1,这一步骤确保每个特征在PCA中具有相同的重要性;
Step 2:计算协方差矩阵:计算标准化后数据的协方差矩阵。协方差矩阵描述了数据特征之间的关系,它的特征值和特征向量是PCA的关键;
Step 3:特征值分解:对协方差矩阵进行特征值分解(或奇异值分解),得到特征值和对应的特征向量。特征向量表示了数据在新的主成分空间中的方向,而特征值则表示了数据在这些方向上的方差;
Step 4:选择主成分:根据特征值的大小,选择最大的k个特征值对应的特征向量作为主成分,其中k是希望保留的新特征空间的维度;
Step 5:数据投影:使用选定的主成分构建投影矩阵,将原始数据投影到新的低维度空间中。新的数据集包含了原始数据在主成分方向上的投影,即进行了降维的数据。
2.核心代码
def pca(X, k):
# 样本中心化,axis=0是计算每一列的均值
X = X - np.mean(X, axis=0)
# 将X变为nxm(5x68040)形式
X = X.T
# 计算协方差矩阵5x5
C_before = (1 / (X.shape[1] - 1)) * np.dot(X, X.T)
# 计算特征值和对应的特征向量矩阵
eigenvalues, eigenvectors = np.linalg.eig(C_before)
# 求出最大的k个特征值的索引,np.argsort返回特征值从小到大排序后的数在原数组中的索引
indexes = np.argsort(eigenvalues)[::-1][:k] # [::-1]将数组倒序,[:k]取前k个值
# 选择特征向量矩阵中列索引在indexes中的所有列构成最终的投影矩阵
P = eigenvectors[:, indexes]
# 将特征向量转为沿行方向排列,P:5x32
P = P.T
# 计算降维结果Y=PX(P:5x32,X:32x68040)
Y = np.dot(P, X)
# 求降维之后的矩阵协方差
# C_after = np.cov(Y, rowvar=1)
# 先把Y变成68040x5,否则不好直接减去均值
Y = Y.T
Y = Y - np.mean(Y, axis=0)
# 再把Y转回5x68040
Y = Y.T
C_after = (1 / (Y.shape[1] - 1)) * np.dot(Y, Y.T)
return C_before, Y, C_after
3.实验结果
测试数据,输入、输出的展示,对输出结果的分析。
(第一个输出是原始导入的数据及其大小,其余在图中均有标注)
4.亮点、分析与总结
在进行PCA算法实验时,关键的步骤和实验设计包括:
-数据预处理:包括数据标准化、缺失值处理等,以确保数据质量和一致性。
-协方差矩阵计算:计算数据的协方差矩阵是PCA的核心步骤之一,它反映了数据特征之间的相关性。
-特征值分解:通过特征值分解或奇异值分解来计算主成分和特征值,以确定保留的主成分数量和方向。
-主成分投影:将数据投影到选定的主成分上,得到降维后的数据表示。
在实验和应用PCA时,遇到以下问题或错误:
-维度选择:选择合适的主成分数量是一个关键问题,选择过多或过少的主成分可能会影响到数据的解释性和模型的性能。
-数据标准化问题:如果数据未正确标准化,可能会导致主成分提取的偏差,影响PCA的效果。
-过拟合和欠拟合:过多保留主成分可能导致过拟合,而选择过少的主成分则可能导致信息损失和欠拟合问题。
-数值稳定性:在计算协方差矩阵和特征值分解时,可能会面临数值稳定性问题,特别是对于大型数据集。
PCA作为一种经典的数据分析和降维技术,在实际应用中具有广泛的用途和效果。理解其功能、改进方法以及在实验和应用中可能遇到的问题,有助于更有效地利用PCA进行数据分析和模型优化。对PCA算法进行深入的实验和分析,可以帮助优化数据处理流程,并提高数据分析和预测模型的性能和解释性。
三、第三次次实验:SIFT图像检索实验
1. 算法描述
SIFT(Scale-Invariant Feature Transform)搜索是一种基于SIFT特征的方法。SIFT特征是一种局部特征,具有尺度不变性和旋转不变性,适合用于图像匹配、物体识别和场景分类等任务。
(1)例如:
-----------------------------------------------------------------------------------------
def SIFT(img):
I = cv2.imread(img)
detector = cv2.SIFT_create()
(_, features) = detector.detectAndCompute(I, None)
# 返回的第一个值是关键点信息,不需要
return features
descriptors = SIFT('E:/vstest/C/corel/0/0.jpg')
desc = []
desc.append(descriptors)
for i in range(10):
for j in range(100):
temp_des=SIFT('E:/vstest/C/corel/'+str(i)+'/'+str(j+i*100)+'.jpg')
desc.append(temp_des) # desc以数组形式保存所有描述子
descriptors = np.vstack((descriptors, temp_des))
descriptors = descriptors[1211:,:]
desc = desc[1:]
descriptors.shape
-----------------------------------------------------------------------------------------
功能:使用SIFT算法从给定的图像中提取关键点和描述子;
算法流程:
输入:图像集corel;
输出:与图像相似的其余图片;
Step 1:创建SIFT特征检测器,然后读取图像并提取SIFT特征,将所有特征点描述子保存为矩阵;
Step 2:使用k-means算法对特征描述子进行聚类,得到一定数量的视觉词汇(聚类中心;
Step 3:对于每张图像,将其特征描述子投射到视觉词汇空间中,得到该图像的直方图向量;
Step 4:根据输入图像的直方图向量,利用余弦相似度计算与所有图像的相似度,并返回最相似的前10幅图像的索引。
2.核心代码
def SIFT(img):
I = cv2.imread(img)
detector = cv2.SIFT_create()
(_, features) = detector.detectAndCompute(I, None) # 返回的第一个值是关键点信息,不需要
return features
descriptors = SIFT('E:/vstest/C/corel/0/0.jpg')
desc = []
desc.append(descriptors)
for i in range(10):
for j in range(100):
temp_des = SIFT('E:/vstest/C/corel/'+str(i)+'/'+str(j+i*100)+'.jpg') # (1,128)向量
desc.append(temp_des) # desc以数组形式保存所有描述子
descriptors = np.vstack((descriptors, temp_des)) # 将所有的特征点描述子堆叠为矩阵(n,128)
descriptors = descriptors[1211:,:]
desc = desc[1:]
descriptors.shape
def kmeans(X, k):
n = X.shape[0] # 数据点总数
epoch = 0 # 迭代次数
idxs = [random.randint(0,k) for _ in range(k)]
Centroids = X[idxs] # 构造初始的中心点矩阵(1000x128)
while(1):
epoch += 1
cls_pos = {}
# 对于每个点,计算它到各中心的距离,得到它所属的类
cls, _ = vq(X, Centroids)
for i in range(n):
cls_pos.setdefault(cls[i],[]).append(X[i]) # cls_pos:以类为key,其value为一个数组,保存属于该类的点坐标
for j in range(k):
if j not in cls_pos.keys():
cls_pos.setdefault(j,[]).append(Centroids[j]) # 若某类没有点被分配进来,则该类只有类中心点一个点
# 所有点均已分到相应的类中。下求新的聚类中心坐标
Centroids_new = calcNewCentroids(cls_pos)
# 当聚类中心不再变化时,停止迭代
if (Centroids_new == Centroids).all():
break
# 更新聚类中心为新的聚类中心
Centroids = Centroids_new
# 返回聚类中心坐标及迭代次数
return epoch, Centroids
3.实验结果
测试数据,输入、输出的展示,对输出结果的分析。
输入的图像ID为600如下:
聚类数设置为1000(100/15.2s 500/26.2s 1000/32s 5000/43.5s)
输出结果如下:
结果显示全是花,正确率百分之百。
输入的图像ID为500如下:
聚类数设置为1000(100/15.2s 500/26.2s 1000/32s 5000/43.5s)
输出结果如下:
结果显示两张不是大象,正确率百分之八十。
4.亮点、分析与总结
定义了余弦距离函数cosin_dist来衡量两幅图像的直方图表示之间的相似度。基于此,输入查询图像的直方图,计算其与数据库中所有图像的相似度,并返回最相似图像的索引。改进的地方比如虽然使用了基本的K-means实现,但通过随机选择初始质心以及迭代直到聚类中心不再改变,保证了聚类质量。进一步的优化可能包括使用K-means++初始化策略减少迭代次数。脚本尝试应用TF-IDF对直方图进行加权,以减少常见视觉词汇的权重,但最终因效果不佳而未采用。这提示在某些场景下,直接的频率统计可能比加权更能反映图像间的关系。系统性能评估显示,对于特定图像类别,检索准确率表现不一,存在一定的类别混淆问题,尤其是在处理具有相似背景或特征的图像时。
四、第四次实验:LSH索引
1. 算法描述
局部敏感哈希(Locality Sensitive Hashing, LSH)是一种高效的数据结构和算法,旨在解决高维空间中的近似最近邻(Approximate Nearest Neighbor, ANN)搜索问题。通过将向量映射到不同的桶中,可以大大减少搜索的时间复杂度,从而提高搜索效率。
其主要功能包括:降维映射、近似相似性判断、可扩展性和效率、灵活性、多级索引结构等。
(1)例如:
-----------------------------------------------------------------------------------------
def find(q, k, R, b, a, buckets):
hash_keys = np.floor((dot(q, R) + b) / a)[0] # 不加[0]返回的是1xtables_num的矩阵,取[0]转为数组
# 遍历q点的索引键列表,找各桶中与其索引键值相等的点
for i, hash_key in enumerate(hash_keys):
if i == 0:
candi_set = set(buckets[0][hash_key])
else:
candi_set = candi_set.union(buckets[i][hash_key]) # 候选集:在各桶中的索引与q相同的点取并集(若取交集,会导致候选集元素个数接近1,不现实)
candi_set = list(candi_set) # 转为list便于遍历,因为'set' object does not support indexing
# 遍历候选集,求出离q最近的k个点并返回
dist = [calc_dist(data[i], q) for i in candi_set]
set_idxes = argsort(dist)[1: k + 1] # set_idxes是近邻点在候选集中的索引,要将其转为在原数据集中的索引
res = [candi_set[i] for i in set_idxes]
return res
-----------------------------------------------------------------------------------------
功能:查询与q相似的向量,并输出相似度最高的k个向量在原数据集中的索引;
算法流程:
输入:必要的库和数据集,其中数据集是一个32维的numpy数组;
输出:搜索算法的准确率、召回率和查准率;
Step 1:预先算好true_label,存入csv文件;
Step 2:定义了一个计算欧氏距离的函数calc_dist,使用了numba库进行加速;
Step 3:定义了hash_and_fill函数,用于将向量映射到各个桶中。该函数接受输入数据、桶的数量、桶的索引矩阵和偏移量作为参数,返回一个包含所有桶的字典列表;
Step 4:find函数用于查找与给定向量最相似的k个向量。它首先计算给定向量在所有桶中的哈希值,然后遍历这些哈希值,将所有桶中与哈希值相同的向量添加到候选集中。最后,计算候选集中向量与给定向量的距离,并返回距离最近的k个向量的索引;
Step 5:使用hash_and_fill函数将数据集中的向量映射到各个桶中,并使用find函数进行相似度搜索。
2.核心代码
# 将向量映射到各桶的索引
def hash_and_fill(inputs, R, b, a):
# 初始化空的hash_table
buckets = [{} for _ in range(bucket_num)]
mapped_idxes = floor((dot(inputs, R) + b) / a) # 68040x10,每一行是这个点在所有桶中的哈希值
for i, hash_keys in enumerate(mapped_idxes):
for j, hash_key in enumerate(hash_keys):
# 每个桶是一个字典,其中的所有key对应该桶的所有索引键值,每个key对应的value是一个list,里面存放映射到该桶、该索引键值的所有点在原数据集的idx
buckets[j].setdefault(hash_key, []).append(i)
return buckets # [ {8: [0, 2, 5,...], 12: [2, 12, 16,...] }, {}, {}, ...]
# 查询与q相似的向量,并输出相似度最高的k个向量在原数据集中的索引
def find(q, k, R, b, a, buckets):
hash_keys = np.floor((dot(q, R) + b) / a)[0] # 不加[0]返回的是1xtables_num的矩阵,取[0]转为数组
# 遍历q点的索引键列表,找各桶中与其索引键值相等的点
for i, hash_key in enumerate(hash_keys):
if i == 0:
candi_set = set(buckets[0][hash_key])
else:
candi_set = candi_set.union(buckets[i][hash_key]) # 候选集:在各桶中的索引与q相同的点取并集(若取交集,会导致候选集元素个数接近1,不现实)
candi_set = list(candi_set) # 转为list便于遍历,因为'set' object does not support indexing
# 遍历候选集,求出离q最近的k个点并返回
dist = [calc_dist(data[i], q) for i in candi_set]
set_idxes = argsort(dist)[1: k + 1] # set_idxes是近邻点在候选集中的索引,要将其转为在原数据集中的索引
res = [candi_set[i] for i in set_idxes]
return res
3.实验结果
测试数据,输入、输出的展示,对输出结果的分析。
4.亮点、分析与总结
hash_and_fill函数通过随机投影矩阵R、偏置b及桶宽a,将高维向量映射至多级哈希桶中。此步骤有效降低了后续搜索的维度,是LSH算法的基础。为了进一步提升效率,可以考虑使用更高效的哈希函数或动态调整桶宽a以平衡查询效率与准确性。在候选集中精确计算欧氏距离以找到最相似的k个点,这是算法的关键计算部分。考虑到效率,已经使用了Numba进行JIT编译加速,但还可以探索如KD树、球树等数据结构进一步优化搜索过程。bucket_num、a等参数的选择对性能影响巨大,需要根据实际数据分布反复试验,寻找最优配置。当前实现展示了LSH的基本框架,但在实际应用中还需针对具体需求和数据特性进行细致调优。未来工作可聚焦于算法效率的提升、参数自动调优机制的引入以及对大规模数据集的处理能力增强。