前言
无监督学习
书接上回准备用无监督学习自动对画面状态进行分类,先准备模型。
使用模型提取图片特征:
self.encoder = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=2, padding=1), # 320x180 → 160x90
nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1), # 160x90 → 80x45
nn.ReLU(),
nn.Conv2d(64, 128, 3, stride=2, padding=1), # 80x45 → 40x23
nn.ReLU(),
nn.Conv2d(128, 256, 3, stride=2, padding=1), # 40x23 → 20x12
nn.ReLU()
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(256, 128, 3, stride=2, padding=1, output_padding=1), # 20x12 → 40x23
nn.ReLU(),
nn.ConvTranspose2d(128, 64, 3, stride=2, padding=1, output_padding=1), # 40x23 → 80x45
nn.ReLU(),
nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1), # 80x45 → 160x90
nn.ReLU(),
nn.ConvTranspose2d(32, 3, 3, stride=2, padding=1, output_padding=1), # 160x90 → 320x180
nn.Sigmoid()
)
使用DBSCAN进行聚类:
scaler = StandardScaler()
features_scaled = scaler.fit_transform(features)
dbscan = DBSCAN(
eps=eps,
min_samples=min_samples,
metric='euclidean',
n_jobs=-1
)
然而对于默认参数eps=0.5, min_samples=5来说,聚类效果并不理想:
INFO:cluster_all_images:聚类结果:
INFO:cluster_all_images: 发现的簇数量: 3
INFO:cluster_all_images: 噪声点数量: 4677
INFO:cluster_all_images: 噪声点比例: 97.62%
INFO:cluster_all_images: 簇 0 的样本数: 63 (1.31%)
INFO:cluster_all_images: 簇 1 的样本数: 40 (0.83%)
INFO:cluster_all_images: 簇 2 的样本数: 11 (0.23%)
绝大部分的图片都被归为了噪声,成簇的样本是一些完全一样的无关紧要的图片:
经过多次尝试,eps=187的时候有比较好的聚类效果:
INFO:cluster_all_images:聚类结果:
INFO:cluster_all_images: 发现的簇数量: 85
INFO:cluster_all_images: 噪声点数量: 630
INFO:cluster_all_images: 噪声点比例: 13.15%
INFO:cluster_all_images: 簇 0 的样本数: 13 (0.27%)
INFO:cluster_all_images: 簇 1 的样本数: 6 (0.13%)
INFO:cluster_all_images: 簇 2 的样本数: 5 (0.10%)
INFO:cluster_all_images: 簇 3 的样本数: 157 (3.28%)
INFO:cluster_all_images: 簇 4 的样本数: 27 (0.56%)
INFO:cluster_all_images: 簇 5 的样本数: 21 (0.44%)
···
INFO:cluster_all_images: 簇 84 的样本数: 25 (0.52%)
此时模型可以把大部分相似的图片聚类为一个簇:
但是同时也发现了问题:
从状态理解上来说,上面都是处于不需要操作的对话状态,但是因为人物和背景的不同,导致画面有较大差异,被聚类成了两个簇。
另一方面,每次进行聚类分簇的结果是不确定的,模型并不能保证上一次的簇1和下一次的簇1相似,因此聚类后还是需要人工去辨别重新整合分类。
特征可视化
于是编写了一个脚本查看每层的特征图:
可以发现相比于经典的图像识别模型,这个模型几乎没有提取到有用的特征,画面的无关数据与关联数据没有区分开来(也有原因是数据集中我都是用一只马娘训练的)
顺便看了看上个状态识别模型的特征图:
(看归看先用着效果不好再解决)
继续无监督学习
既然一次性分类所有截图不太可能,把两个界面分开感觉还是有希望的。刚好上个状态识别模型中,我把养成主界面和训练主界面放在了一起,这次看看能不能把他们通过无监督学习分开来:
对于这两个界面来说,最大的区别在于下半部分的按钮区域,于是先对数据进行裁剪,只保留不一样的部分:
x, y, w, h = self.training_area
image = image.crop((x, y, x + w, y + h))
使用Kmeans进行聚类:
with torch.no_grad():
for images, paths in data_loader:
images = images.to(device)
encoded, _ = model(images)
# 展平特征
encoded = encoded.view(encoded.size(0), -1) # [batch_size, 256*20*12]
features.extend(encoded.cpu().numpy())
image_paths.extend(paths)
features = np.array(features) # [N, 256*20*12]
logger.info(f"原始特征维度: {features.shape}")
# 移除零方差特征
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.0)
features = selector.fit_transform(features)
logger.info(f"移除零方差特征后的维度: {features.shape}")
# 标准化特征
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
features = scaler.fit_transform(features)
# 使用PCA降维
from sklearn.decomposition import PCA
# 保留95%的方差
pca = PCA(n_components=0.95)
features = pca.fit_transform(features)
logger.info(f"PCA降维后的维度: {features.shape}")
logger.info(f"保留的方差比例: {sum(pca.explained_variance_ratio_):.3f}")
logger.info(f"最终特征值范围: [{features.min():.3f}, {features.max():.3f}]")
# 使用更稳定的KMeans参数
from sklearn.cluster import KMeans
kmeans = KMeans(
n_clusters=2,
random_state=42,
n_init=20, # 增加初始化次数
max_iter=500, # 增加最大迭代次数
tol=1e-4, # 调整收敛阈值
algorithm='lloyd' # 使用Lloyd算法,更稳定但更慢
)
INFO:cluster_images:原始特征维度: (1198, 61440)
INFO:cluster_images:移除零方差特征后的维度: (1198, 51883)
INFO:cluster_images:PCA降维后的维度: (1198, 336)
INFO:cluster_images:保留的方差比例: 0.950
INFO:cluster_images:最终特征值范围: [-290.572, 872.163]
INFO:cluster_images:聚类完成,轮廓系数: 0.174
INFO:cluster_images:聚类惯性: 49129444.000
INFO:cluster_images:类别 0 的样本数: 861
INFO:cluster_images:类别 1 的样本数: 337
效果很好,训练界面和养成主界面被完美的区分开了:
这样就可以进一步用标注数据去训练状态识别模型了。
下一步
数据量越来越多导致训练速度逐渐减缓,而因为数据是每秒采集一次的,数据集中有大量重复的图片,下一步准备先对数据集进行一次清洗再改进状态识别模型。