猫狗分类

数据增强(仅作展示)

8000的数据量增强后就得到40000张图片了。

从左到右分别是:原图(1),左右反转(2),几何随机反转(3),对比度变化(4),噪音变化(5)。

模型与算法

模型主要分为4个内容,

特征提取:HOG方向梯度直方图,LBP。

降维:PCA主成分分析法。

分类模型:SVM支持向量机。

HOG(Histogram of Oriented Gradients)

HOG(方向梯度直方图)是一种描述图像局部形状特征的方式,广泛用于物体检测任务中(如行人检测)。HOG 的核心思想是:利用图像局部区域的梯度方向分布来描述物体的边缘信息

  1. 灰度化图像
    因为边缘信息主要由强度变化决定,所以通常将彩色图像转换为灰度图。

  2. 计算梯度(常用Sobel算子)
    对图像进行横向(x)和纵向(y)方向的梯度计算:

    得到每个像素点的梯度幅值和方向:

  3. 划分 cells(如8×8像素)

  4. 计算每个 cell 内的方向直方图

    • 将方向范围划分成 bins(例如:9个 bin 表示 0° 到 180°)
    • 每个像素对某个 bin 投票,权重是梯度幅值
  5. 划分 blocks 并进行归一化
    为了抵抗光照变化,把多个 cell 合并为一个 block(如 2×2 cells = 16×16 像素),并对该 block 的特征向量进行 L2 归一化:

  6. 连接所有 block 的特征向量,得到整个图像的 HOG 描述子

核心代码(参考featues.py中的extract_hog函数)

我自己用numpy纯用for循环写了LBP和hog,发现速度慢的一批,询问gpt告诉我用skimage会快几十倍。主要是因为它使用了底层的高效实现,比如 C/C++、多线程 SIMD 优化,以及避免 Python 中循环的开销。侧面印证了,在python使用for循环是及其低效的行为。

LBP(Local Binary Pattern)

LBP 是一种简单却非常有效的纹理描述算子,它通过对像素邻域灰度的比较,生成一个二进制编码,从而反映图像的局部纹理特征。对于图像中每一个像素,LBP 通过与其邻域像素进行灰度比较,编码成一个二进制数。这个数反映了像素的局部纹理结构。

🔧 实现步骤

以 3×3 邻域为例:

  1. 选定一个中心像素点 I_c

  2. 比较邻域中每个像素 I_p 与中心像素:

  3. 将比较结果按顺时针顺序排列,形成一个 8 位二进制数

    例如:

    1
    2
    3
    复制编辑
    邻域比较结果:1 0 1 1 0 0 1 1
    LBP编码:0b10110011 = 179
  4. 该 LBP 值作为中心像素的纹理编码

  5. 可选:在图像上滑动窗口,统计各区域的 LBP 值直方图作为特征

核心代码(参考featues.py中的extract_lbp函数)

相关超参数

1
2
3
4
5
6
7
pca_components = 200   # PCA降维维度
svm_lr = 0.01 # SVM学习率
svm_epochs = 100 # SVM训练轮数
svm_C = 0.5 # 正则项强度
batch_size = 32 # 小批量大小
momentum = 0.9 # 动量系数

特征处理结果展示:

Randomized PCA(随机近似PCA)降维

将高维特征投影到低维空间,保留尽可能多的方差信息。适用于加速分类器训练、去噪或可视化等。

随机近似PCA通过引入随机投影来近似传统PCA的计算过程,显著降低计算复杂度。具体步骤如下:数据去中心化,随机投影,迭代改进,QR分解,求解主成分。

why不用传统PCA,因为有点慢,gpt给了这个方法比较快。

核心代码 (main.py中的PCA类)

带动量项的线性支持向量机(Linear SVM)分类器

优化处:带动量,小个batch进行运算。

理解:我自己第一次用的的时候想说,二分类应该是二元交叉熵才对(深度学习常见分类loss)。其实是不对的,SVM最核心的思想就是把两个分类隔开,找到一个能最大化分类间隔(margin)的分界面。SVM 更关心的是“有没有足够的安全边界”,而不是概率预测的准确度。hinge loss合页损失 天生就和“间隔最大化”是一致的。

基本原理

对于样本$(x_i, y_i)$,其中 $y_i \in {-1, 1}$,构造一个线性分类器:

目标函数(Hinge Loss + L2正则):
  • 第一项:最大化间隔(使 w 越小越好)
  • 第二项:对“错分样本”进行惩罚
  • C:控制对错分的容忍程度(大 = 更少错误)

核心代码(main.py中的class LinearSVM)

相关超参数

1
2
3
4
5
6
pca_components = 200   # PCA降维维度
svm_lr = 0.01 # SVM学习率
svm_epochs = 100 # SVM训练轮数
svm_C = 0.5 # 正则项强度
batch_size = 32 # 小批量大小
momentum = 0.9 # 动量系数

结果展示

结果很差,主要是这个训练效果看着跟没有一样,没有做到像老师一样有递增的曲线,不太清楚是为什么。问gpt也云里雾里。但至少速度跟上了,除了在提取特征时因为图片太多,比较慢。

运行结果:

训练时的loss

会生成一个prediction_results.csv具有输出的结果。

filename predicted_label
cat.4001.jpg 0
cat.4002.jpg 1
cat.4003.jpg 0
cat.4004.jpg 1
。。。。 。。。。
dog.4704.jpg 0
dog.4705.jpg 0
dog.4706.jpg 1

局限性

1:特征组合:PCA降维方法,将特征只是堆叠起来,未能做到有机组合。

2:数据处理:猫狗分类时图片大小不一,应该考虑合适的resize和归一化处理方法。

3:SVM:训练曲线有明显问题,不清楚怎么解决。

代码展示

文件位置

utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from skimage import exposure, util, transform
import matplotlib
matplotlib.use('TkAgg')

class StandardScaler:
def __init__(self):
self.mean = None
self.std = None

def fit(self, X):
self.mean = np.mean(X, axis=0)
self.std = np.std(X, axis=0)
# 为了防止除以 0
self.std[self.std == 0] = 1e-8

def transform(self, X):
return (X - self.mean) / self.std

def fit_transform(self, X):
self.fit(X)
return self.transform(X)
def load_and_augment_images(cat_dir, dog_dir, image_size=(128, 128)):
images = []
labels = []

def center_normalize(img):
"""将图像做中心归一化(减均值除方差)"""
mean = np.mean(img)
std = np.std(img) + 1e-7
return (img - mean) / std

def read_and_process_image(path, label):
img = cv2.imread(path)
if img is None:
return
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, image_size)
img = img.astype(np.float32) / 255.0 # 缩放到 [0, 1]

# 原图
images.append(center_normalize(img))
labels.append(label)

# 几何增强:水平翻转
flipped = np.fliplr(img)
images.append(center_normalize(flipped))
labels.append(label)

# 几何增强:随机旋转
angle = np.random.uniform(-25, 25)
rotated = transform.rotate(img, angle, mode='edge') # 仍为float32
images.append(center_normalize(rotated))
labels.append(label)

# 色彩增强:对比度调整
contrast = exposure.adjust_gamma(img, gamma=0.8)
images.append(center_normalize(contrast))
labels.append(label)

# 色彩增强:添加噪声
noisy = util.random_noise(img)
images.append(center_normalize(noisy))
labels.append(label)

# 限制读取前1000张猫图
cat_count = 0
for filename in sorted(os.listdir(cat_dir)):
if filename.startswith("cat") and filename.endswith(".jpg"):
read_and_process_image(os.path.join(cat_dir, filename), label=0)
cat_count += 1
if cat_count >= 4000:
break

# 限制读取前1000张狗图
dog_count = 0
for filename in sorted(os.listdir(dog_dir)):
if filename.startswith("dog") and filename.endswith(".jpg"):
read_and_process_image(os.path.join(dog_dir, filename), label=1)
dog_count += 1
if dog_count >= 4000:
break

return np.array(images), np.array(labels)

def load_images_for_prediction(cat_dir, dog_dir, image_size=(128, 128)):
images = []
labels = []
filenames = [] # 记录图像名,便于生成Excel

def center_normalize(img):
mean = np.mean(img)
std = np.std(img) + 1e-7
return (img - mean) / std

def read_and_process_image(path, label):
img = cv2.imread(path)
if img is None:
return
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, image_size)
img = img.astype(np.float32) / 255.0
img = center_normalize(img)

images.append(img)
labels.append(label)
filenames.append(os.path.basename(path))

# 猫图
for filename in sorted(os.listdir(cat_dir)):
if filename.startswith("cat") and filename.endswith(".jpg"):
read_and_process_image(os.path.join(cat_dir, filename), label=0)

# 狗图
for filename in sorted(os.listdir(dog_dir)):
if filename.startswith("dog") and filename.endswith(".jpg"):
read_and_process_image(os.path.join(dog_dir, filename), label=1)

return np.array(images), np.array(labels), filenames

def load_images(cat_dir, dog_dir, return_filenames=False):
data = []
labels = []
filenames = []

for label, path in zip([0, 1], [cat_dir, dog_dir]):
for fname in os.listdir(path):
fpath = os.path.join(path, fname)
img = cv2.imread(fpath, cv2.IMREAD_GRAYSCALE)
if img is not None:
img = cv2.resize(img, (128, 128))
data.append(img)
labels.append(label)
if return_filenames:
filenames.append(fname)

if return_filenames:
return data, labels, filenames
return data, labels
# ✅ 测试代码
if __name__ == "__main__":
cats_path = 'data/data/train/cats'
dogs_path = 'data/data/train/dogs'
X, y = load_and_augment_images(cats_path, dogs_path)

print("图像数量:", len(X), "标签数量:", len(y)) # 应为 5 * 1000 * 2 = 10000

for i in range(5):
plt.subplot(1, 5, i + 1)
# 还原为 RGB 图显示
img = X[i]
img = ((img - img.min()) / (img.max() - img.min())) # 归一化显示
plt.imshow(img)
plt.title('Cat' if y[i] == 0 else 'Dog')
plt.axis('off')
plt.tight_layout()
plt.show()

fetures.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import numpy as np
import cv2
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('TkAgg')

from skimage.feature import hog, local_binary_pattern
from joblib import Parallel, delayed

# ------ SIFT:OpenCV自带的即可,后舍弃 ------
def extract_sift(img, max_features=100):
img = (img * 255).clip(0, 255).astype(np.uint8)
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
sift = cv2.SIFT_create()
keypoints, descriptors = sift.detectAndCompute(gray, None)
if descriptors is None:
return np.zeros((max_features, 128))
if descriptors.shape[0] > max_features:
descriptors = descriptors[:max_features]
else:
pad = np.zeros((max_features - descriptors.shape[0], 128))
descriptors = np.vstack((descriptors, pad))
return descriptors # (max_features, 128)


# ------ HOG:skimage实现 ------
def extract_hog(img, pixels_per_cell=(8, 8), cells_per_block=(2, 2), orientations=9):
img = (img * 255).clip(0, 255).astype(np.uint8)
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
features = hog(
gray,
orientations=orientations,
pixels_per_cell=pixels_per_cell,
cells_per_block=cells_per_block,
block_norm='L2-Hys',
transform_sqrt=True,
feature_vector=True
)
return features


# ------ LBP:skimage实现 ------
def extract_lbp(img, P=8, R=1):
img = (img * 255).clip(0, 255).astype(np.uint8)
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
lbp_img = local_binary_pattern(gray, P, R, method='uniform')
n_bins = P + 2
hist, _ = np.histogram(lbp_img.ravel(), bins=n_bins, range=(0, n_bins))
hist = hist.astype(np.float32)
hist /= (hist.sum() + 1e-6)
return hist, lbp_img


# ------ 批处理和并行提取 ------
def extract_features_batch(images, feature_type='hog', n_jobs=-1):
if feature_type == 'sift':
return np.array(Parallel(n_jobs=n_jobs)(delayed(extract_sift)(img) for img in images))
elif feature_type == 'hog':
return np.array(Parallel(n_jobs=n_jobs)(delayed(extract_hog)(img) for img in images))
elif feature_type == 'lbp':
results = Parallel(n_jobs=n_jobs)(delayed(extract_lbp)(img) for img in images)
return np.array([r[0] for r in results]) # 仅返回hist
else:
raise ValueError("Unsupported feature type: " + feature_type)


# ------ 测试用例 ------
if __name__ == "__main__":
from utils import load_and_augment_images

cats_path = 'data/data/train/cats'
dogs_path = 'data/data/train/dogs'
X, y = load_and_augment_images(cats_path, dogs_path)
sample_img = X[0]

sift_desc = extract_sift(sample_img)
print("SIFT shape:", sift_desc.shape)

hog_feat = extract_hog(sample_img)
print("HOG shape:", hog_feat.shape)

lbp_hist, lbp_img = extract_lbp(sample_img)
print("LBP hist shape:", lbp_hist.shape)

# 可视化
plt.figure(figsize=(12, 4))

plt.subplot(1, 4, 1)
plt.imshow(sample_img)
plt.title("Original")
plt.axis('off')

plt.subplot(1, 4, 2)
plt.plot(hog_feat[:100]) # 展示前100维
plt.title("HOG Feature (partial)")

plt.subplot(1, 4, 3)
plt.imshow(lbp_img, cmap='gray')
plt.title("LBP Image")
plt.axis('off')

plt.subplot(1, 4, 4)
plt.plot(lbp_hist)
plt.title("LBP Histogram")

plt.tight_layout()
plt.show()

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import os
import time
import numpy as np
import matplotlib.pyplot as plt
import pickle
from tqdm import tqdm
from utils import load_and_augment_images,load_images,load_images_for_prediction
from features import extract_hog, extract_lbp
import matplotlib
matplotlib.use('TkAgg')
import pandas as pd
# ===== 超参数设置 =====
pca_components = 100 # PCA降维维度
svm_lr = 0.01 # SVM学习率
svm_epochs = 100 # SVM训练轮数
svm_C = 0.1 # 正则项强度
batch_size = 32 # 小批量大小
momentum = 0.9 # 动量系数

# ===== 特征标准化 =====
def standardize_train(X):
mean = np.mean(X, axis=0)
std = np.std(X, axis=0) + 1e-8
return (X - mean) / std, mean, std

def standardize_test(X, mean, std):
return (X - mean) / std
# ===== 随机近似 PCA 实现 =====
def randomized_pca(X, n_components, n_iter=3):
from numpy.random import randn
X = X - np.mean(X, axis=0) # 去中心化
m, n = X.shape # m: 样本数, n: 特征维度
Q = randn(n, n_components + 10) # 生成随机投影矩阵(扩展10维用于增强稳定性)
Y = X @ Q # 初步低维投影(N x k)

# 功能增强迭代(提高拟合精度)
for _ in range(n_iter):
Y = X @ (X.T @ Y) # 使用 power iteration 提高近似质量

from numpy.linalg import qr
Q, _ = qr(Y) # 正交化(N x k)

B = Q.T @ X # 小矩阵 B(k x D)
U_hat, S, Vt = np.linalg.svd(B, full_matrices=False) # 奇异值分解(低成本)
components = Vt[:n_components].T # 获取主成分(D x k)

return components

# ===== PCA 类封装 =====
class PCA:
def __init__(self, n_components):
self.n_components = n_components # 要保留的主成分数量
self.mean = None # 特征均值(用于中心化)
self.components = None # 主成分方向(D x k)

def fit(self, X):
self.mean = np.mean(X, axis=0) # 计算特征均值
X_centered = X - self.mean # 去中心化
self.components = randomized_pca(X_centered, self.n_components) # 求解主成分

def transform(self, X):
return (X - self.mean) @ self.components # 投影到主成分方向上

def fit_transform(self, X):
self.fit(X)
return self.transform(X)

# ===== Hinge Loss SVM 实现(标准 + 动量) =====
class LinearSVM:
def __init__(self, lr=0.01, epochs=100, C=1.0, momentum=0.9):
# 初始化学习率、迭代轮数、正则化系数、动量系数
self.lr = lr
self.epochs = epochs
self.C = C # 正则化强度(越大越复杂)
self.momentum = momentum
self.w = None # 权重向量
self.b = 0 # 偏置项
self.loss_history = [] # 保存每轮的损失
self.acc_history = [] # 保存每轮的准确率

def fit(self, X, y):
n_samples, n_features = X.shape
self.w = np.zeros(n_features)
self.b = 0
y_ = np.where(y == 1, 1, -1) # 将标签从 0/1 转换为 -1/1

v_w = np.zeros_like(self.w) # 初始化动量项
v_b = 0

indices = np.arange(n_samples) # 用于打乱数据顺序

for epoch in tqdm(range(self.epochs), desc="Training SVM"):
np.random.shuffle(indices) # 每轮打乱样本顺序
X, y_ = X[indices], y_[indices]

loss = 0
correct = 0

for i in range(0, n_samples, batch_size):
# mini-batch
X_batch = X[i:i + batch_size]
y_batch = y_[i:i + batch_size]

# 计算 hinge 损失边界:margin = y(w·x + b)
margins = y_batch * (np.dot(X_batch, self.w) + self.b)

# 找出 margin < 1 的样本(即违反间隔约束的)
mask = margins < 1

# 梯度计算:L2 正则项 + hinge 损失梯度
dw = 2 * self.w - self.C * np.dot((y_batch[mask])[:, np.newaxis].T, X_batch[mask]).flatten()
db = -self.C * np.sum(y_batch[mask])

# 动量更新
v_w = self.momentum * v_w + self.lr * dw
v_b = self.momentum * v_b + self.lr * db

# 参数更新
self.w -= v_w
self.b -= v_b

# 计算损失和精度
loss += np.mean(np.maximum(0, 1 - margins)) + self.C * np.sum(self.w ** 2)
y_pred = np.sign(np.dot(X_batch, self.w) + self.b)
correct += np.sum(y_pred == y_batch)

# 记录每轮的损失与精度
self.loss_history.append(loss / n_samples)
self.acc_history.append(correct / n_samples)

def predict(self, X):
# 预测标签:大于0为正类(标签1),否则为负类(标签0)
return np.where(np.dot(X, self.w) + self.b >= 0, 1, 0)

# ===== 特征提取(去除SIFT,仅保留HOG+LBP) =====
def normalize(features):
mean = np.mean(features, axis=0)
std = np.std(features, axis=0)
return (features - mean) / (std + 1e-8)
'''
def extract_features(X):
all_features = []
for img in tqdm(X, desc="Extracting features"):
hog = extract_hog(img)
lbp, _ = extract_lbp(img)
# 只是堆叠了起来
combined = np.concatenate([hog, lbp])
all_features.append(combined)
return np.array(all_features)
'''

def extract_features(X):
all_features = []
for img in tqdm(X, desc="Extracting features"):
hog = extract_hog(img)
lbp, _ = extract_lbp(img)

# 标准化特征
hog_norm = normalize(hog)
lbp_norm = normalize(lbp)

# 特征加权组合
combined = np.concatenate([hog_norm, lbp_norm])
all_features.append(combined)
return np.array(all_features)
# ===== 可视化训练曲线 =====
def plot_training_curve(loss, acc, save_path="svm_training_plot.png"):
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(loss, label='Loss')
plt.title("Hinge Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.grid(True)

plt.subplot(1, 2, 2)
plt.plot(acc, label='Accuracy', color='green')
plt.title("Training Accuracy")
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.grid(True)

plt.tight_layout()
plt.savefig(save_path)
print(f"✅ 训练曲线已保存为: {save_path}")

# ===== 模型保存 =====
def save_model(pca_obj, svm_obj, path="model.pkl"):
with open(path, "wb") as f:
pickle.dump({
'pca': {
'mean': pca_obj.mean,
'components': pca_obj.components,
'n_components': pca_obj.n_components,
},
'svm': {
'w': svm_obj.w,
'b': svm_obj.b,
'lr': svm_obj.lr,
'epochs': svm_obj.epochs,
'C': svm_obj.C,
}
}, f)
print(f"✅ 模型已保存为: {path}")

# ===== 主程序 =====
if __name__ == "__main__":
start_time = time.time()

print("🧾 加载训练图像并增强...")
cats_path = 'data/data/train/cats'
dogs_path = 'data/data/train/dogs'
X, y = load_and_augment_images(cats_path, dogs_path)

print("🔍 提取图像特征...")
X_feat = extract_features(X)

print("📐 标准化特征...")
X_feat, feat_mean, feat_std = standardize_train(X_feat)

print("📉 进行PCA降维...")
pca = PCA(n_components=pca_components)
X_reduced = pca.fit_transform(X_feat)

print("🏋️‍♂️ 训练SVM分类器...")
svm = LinearSVM(lr=svm_lr, epochs=svm_epochs, C=svm_C, momentum=momentum)
svm.fit(X_reduced, np.array(y))

print("🔎 评估训练准确率...")
y_pred = svm.predict(X_reduced)
acc = np.mean(y_pred == np.array(y))
print(f"✅ 最终训练准确率: {acc:.4f}")

print("📊 绘制训练误差与准确率图...")
plot_training_curve(svm.loss_history, svm.acc_history)

print("💾 保存 PCA 和 SVM 模型...")
save_model(pca, svm)

elapsed = time.time() - start_time
print(f"\n⏱️ 总训练耗时: {elapsed:.2f} 秒")
print("📁 加载测试集...")
X_test, y_test, filenames = load_images_for_prediction(
cat_dir="data/data/test/cats",
dog_dir="data/data/test/dogs",
image_size=(128, 128)
)
print("🔎 提取测试特征...")
X_test_feat = extract_features(X_test)
X_test_feat = standardize_test(X_test_feat, feat_mean, feat_std)
X_test_pca = pca.transform(X_test_feat)
print("📈 测试集预测中...")
y_pred = svm.predict(X_test_pca)

# ===== 保存预测结果为 Excel =====
print("📄 保存预测结果至 Excel...")
df = pd.DataFrame({
"filename": filenames,
"predicted_label": y_pred
})
df.to_csv("prediction_results.csv", index=False)
print("✅ 已保存至 svm_test_predictions.csv")

acc = np.mean(y_pred == np.array(y_test))
print(f"✅ 测试集准确率: {acc:.4f}")
print(f"⏱️ 总耗时: {time.time() - start_time:.2f} 秒")

requirements.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
anyio==4.9.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==3.0.0
async-lru==2.0.5
attrs==25.3.0
babel==2.17.0
beautifulsoup4==4.13.3
bleach==6.2.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
colorama==0.4.6
comm==0.2.2
contourpy==1.3.0
cycler==0.12.1
debugpy==1.8.13
decorator==5.2.1
defusedxml==0.7.1
et_xmlfile==2.0.0
exceptiongroup==1.2.2
executing==2.2.0
fastjsonschema==2.21.1
fonttools==4.57.0
fqdn==1.5.1
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
imageio==2.37.0
importlib_metadata==8.6.1
importlib_resources==6.5.2
ipykernel==6.29.5
ipython==8.18.1
ipywidgets==8.1.5
isoduration==20.11.0
jedi==0.19.2
Jinja2==3.1.6
joblib==1.4.2
json5==0.12.0
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
jupyter==1.1.1
jupyter-console==6.6.3
jupyter-events==0.12.0
jupyter-lsp==2.2.5
jupyter_client==8.6.3
jupyter_core==5.7.2
jupyter_server==2.15.0
jupyter_server_terminals==0.5.3
jupyterlab==4.3.6
jupyterlab_pygments==0.3.0
jupyterlab_server==2.27.3
jupyterlab_widgets==3.0.13
kiwisolver==1.4.7
lazy_loader==0.4
MarkupSafe==3.0.2
matplotlib==3.9.4
matplotlib-inline==0.1.7
mistune==3.1.3
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
nest-asyncio==1.6.0
networkx==3.2.1
notebook==7.3.3
notebook_shim==0.2.4
numpy==2.0.2
opencv-python==4.11.0.86
openpyxl==3.1.5
overrides==7.7.0
packaging==24.2
pandas==2.2.3
pandocfilters==1.5.1
parso==0.8.4
pillow==11.1.0
platformdirs==4.3.7
prometheus_client==0.21.1
prompt_toolkit==3.0.50
psutil==7.0.0
pure_eval==0.2.3
pycparser==2.22
Pygments==2.19.1
pyparsing==3.2.3
python-dateutil==2.9.0.post0
python-json-logger==3.3.0
pytz==2025.2
pywin32==310
pywinpty==2.0.15
PyYAML==6.0.2
pyzmq==26.4.0
referencing==0.36.2
requests==2.32.3
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.24.0
scikit-image==0.24.0
scipy==1.13.1
Send2Trash==1.8.3
six==1.17.0
sniffio==1.3.1
soupsieve==2.6
stack-data==0.6.3
terminado==0.18.1
tifffile==2024.8.30
tinycss2==1.4.0
tk==0.1.0
tomli==2.2.1
tornado==6.4.2
tqdm==4.67.1
traitlets==5.14.3
types-python-dateutil==2.9.0.20241206
typing_extensions==4.13.1
tzdata==2025.2
uri-template==1.3.0
urllib3==2.3.0
wcwidth==0.2.13
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.8.0
widgetsnbextension==4.0.13
zipp==3.21.0