增加未知类别负样本

This commit is contained in:
2025-10-23 16:05:17 +08:00
parent 1c8ceef5b0
commit 519bd1c189
4 changed files with 101 additions and 44 deletions
+13 -10
View File
@@ -65,13 +65,13 @@ class EmbeddingFoodClassifierApp:
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# model_path = "../model/embedding_20251011_133653/best_embedding_model.pth"
# model_path = os.path.join(BASE_DIR, "../model/embedding_20251011_133653/best_embedding_model.pth")
model_path = os.path.join(BASE_DIR, "../model/WholeIngredientRecognition/embedding_20251021_085915/best_embedding_model.pth")
model_path = os.path.join(BASE_DIR, "../model/WholeIngredientRecognition/embedding_20251023_120531/best_embedding_model.pth")
# model_path = os.path.join(BASE_DIR, "../model/DishClassification/embedding_20251022_093635/best_embedding_model.pth")
# FAISS索引目录
# index_dir = "../faiss_vector_db/faiss_index"
# index_dir = os.path.join(BASE_DIR, "../faiss_vector_db/faiss_index")
index_dir = os.path.join(BASE_DIR, "../faiss_vector_db/WholeIngredientRecognition/faiss_index")
index_dir = os.path.join(BASE_DIR, "../faiss_vector_db/WholeIngredientRecognition/faiss_index102402")
# index_dir = os.path.join(BASE_DIR, "../faiss_vector_db/DishClassification/faiss_index")
if os.path.exists(model_path) and os.path.exists(index_dir):
@@ -1087,12 +1087,13 @@ class EmbeddingFoodClassifierApp:
"""识别所有图片"""
try:
self.current_results.clear()
score_list = []
for i, img_info in enumerate(self.uploaded_images):
# 使用embedding相似度识别
if self.model is not None and self.faiss_index is not None:
# 使用真实的embedding模型和FAISS索引
predicted_class, confidence, similar_images = self.predict_with_embedding(img_info['image'])
predicted_class, confidence, similar_images,score = self.predict_with_embedding(img_info['image'])
score_list.append(score)
else:
# 模拟预测结果
predicted_class = np.random.choice(self.class_names)
@@ -1122,7 +1123,7 @@ class EmbeddingFoodClassifierApp:
# 更新UI(在主线程中)
self.root.after(0, self.update_progress, i + 1, len(self.uploaded_images))
print('socre_list',score_list)
# 识别完成,更新UI
self.root.after(0, self.recognition_completed)
@@ -1144,9 +1145,11 @@ class EmbeddingFoodClassifierApp:
# 在FAISS索引中搜索最相似的k张图片
scores, indices = self.faiss_index.search(query_embedding, k)
print("最相似的图片索引:", indices)
print("最相似的图片分数:", scores)
# print("最相似的图片索引:", indices)
# print("最相似的图片分数:", type(scores[0]))
# print("最相似的图片分数:", scores[0,0])
# print("最相似的图片分数:", type(scores[0,0]))
# 收集相似图片的类别
similar_classes = []
similar_images = []
@@ -1179,12 +1182,12 @@ class EmbeddingFoodClassifierApp:
vote_ratio = class_counts[predicted_class] / len(similar_classes)
confidence = max_score * vote_ratio
return predicted_class, confidence, similar_images
return predicted_class, confidence, similar_images,round(scores[0,0],3)
else:
# 如果没有找到相似图片,随机选择一个类别
predicted_class = np.random.choice(self.class_names)
confidence = 0.1
return predicted_class, confidence, []
return predicted_class, confidence, [],scores[0,0]
except Exception as e:
print(f"Embedding预测出错: {e}")
+6 -6
View File
@@ -480,12 +480,12 @@ def main():
"""主函数"""
# 配置参数
# MODEL_PATH = "../model/embedding_20251011_133653/best_embedding_model.pth"
# MODEL_PATH = "../model/WholeIngredientRecognition/embedding_20251021_085915/best_embedding_model.pth"
MODEL_PATH = "../model/DishClassification/embedding_20251022_093635/best_embedding_model.pth"
# TRAIN_DIR = "../dataset/WholeIngredientRecognition/train"
TRAIN_DIR = "../dataset/DishClassification/train"
# OUTPUT_DIR = "WholeIngredientRecognition/faiss_index"
OUTPUT_DIR = "DishClassification/faiss_index"
MODEL_PATH = "../model/WholeIngredientRecognition/embedding_20251021_085915/best_embedding_model.pth"
# MODEL_PATH = "../model/DishClassification/embedding_20251022_093635/best_embedding_model.pth"
TRAIN_DIR = "../dataset/WholeIngredientRecognition/train"
# TRAIN_DIR = "../dataset/DishClassification/train"
OUTPUT_DIR = "WholeIngredientRecognition/faiss_index"
# OUTPUT_DIR = "DishClassification/faiss_index"
BATCH_SIZE = 16
INDEX_TYPE = 'flat' # 'flat', 'ivf', 'hnsw'
EMBEDDING_DIM = 512
+4 -4
View File
@@ -162,10 +162,10 @@ def plot_2d(Z: np.ndarray, y: np.ndarray, title: str, out_path: Optional[str] =
def main():
parser = argparse.ArgumentParser(description="可视化高维 embedding 并进行坍塌诊断")
parser.add_argument("--embeddings", type=str, default=os.path.join("DishClassification/faiss_index", "embeddings.json"), help="embeddings.json 路径")
# parser.add_argument("--embeddings", type=str, default=os.path.join("WholeIngredientRecognition/faiss_index", "embeddings.json"), help="embeddings.json 路径")
parser.add_argument("--labels", type=str, default=os.path.join("DishClassification/faiss_index", "labels.json"), help="labels.json 路径")
# parser.add_argument("--labels", type=str, default=os.path.join("WholeIngredientRecognition/faiss_index", "labels.json"), help="labels.json 路径")
# parser.add_argument("--embeddings", type=str, default=os.path.join("DishClassification/faiss_index", "embeddings.json"), help="embeddings.json 路径")
parser.add_argument("--embeddings", type=str, default=os.path.join("WholeIngredientRecognition/faiss_index", "embeddings.json"), help="embeddings.json 路径")
# parser.add_argument("--labels", type=str, default=os.path.join("DishClassification/faiss_index", "labels.json"), help="labels.json 路径")
parser.add_argument("--labels", type=str, default=os.path.join("WholeIngredientRecognition/faiss_index", "labels.json"), help="labels.json 路径")
parser.add_argument("--method", type=str, default="pca", choices=["pca", "tsne", "umap"], help="降维方法")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--max_points", type=int, default=None, help="抽样上限,避免t-SNE/UMAP过慢;None为全量")
+78 -24
View File
@@ -32,6 +32,7 @@ class TaskConfig:
name: str
train_dir: str
val_dir: str
unknown_dir: str # 未知类别数据目录
embedding_dim: int
batch_size: int
lr: float
@@ -45,6 +46,7 @@ TASKS = {
name="DishClassification",
train_dir=os.path.join(settings.BASE_DIR, "dataset", "DishClassification", "train"),
val_dir=os.path.join(settings.BASE_DIR, "dataset", "DishClassification", "val"),
unknown_dir=os.path.join(settings.BASE_DIR, "dataset", "Unknown", "DishClassification","train"), # 未知类别食物目录
embedding_dim=512,
batch_size=16,
lr=1e-3,
@@ -58,6 +60,7 @@ TASKS = {
name="WholeIngredientRecognition",
train_dir=os.path.join(settings.BASE_DIR, "dataset", "WholeIngredientRecognition", "train"),
val_dir=os.path.join(settings.BASE_DIR, "dataset", "WholeIngredientRecognition", "val"),
unknown_dir=os.path.join(settings.BASE_DIR, "dataset", "Unknown","WholeIngredientRecognition", "train"), # 未知类别食物目录
embedding_dim=512,
batch_size=32,
lr=8e-4,
@@ -70,6 +73,7 @@ TASKS = {
name="ProcessedIngredientRecognition",
train_dir=os.path.join(settings.BASE_DIR, "dataset", "ProcessedIngredientRecognition", "train"),
val_dir=os.path.join(settings.BASE_DIR, "dataset", "ProcessedIngredientRecognition", "val"),
unknown_dir=os.path.join(settings.BASE_DIR, "dataset", "Unknown", "ProcessedIngredientRecognition","train"), # 未知类别食物目录
embedding_dim=512,
batch_size=16,
lr=1e-3,
@@ -142,27 +146,33 @@ class TripletDataset(Dataset):
三元组数据集,用于三元组损失训练
每个样本包含:锚点(anchor)、正样本(positive)、负样本(negative)
"""
def __init__(self, dataset_path: str, transform=None, samples_per_class: int = 100):
def __init__(self, dataset_path: str, transform=None, samples_per_class: int = 100, unknown_dir: str = None, unknown_prob: float = 0.7):
"""
初始化三元组数据集
Args:
dataset_path: 数据集路径
transform: 数据变换
samples_per_class: 每个类别最多使用的样本数
unknown_dir: 未知类别数据目录
unknown_prob: 使用未知类别作为负样本的概率
"""
self.dataset_path = dataset_path
self.transform = transform
self.samples_per_class = samples_per_class
self.unknown_dir = unknown_dir
self.unknown_prob = unknown_prob
# 加载数据集
self.class_to_idx = {}
self.idx_to_class = {}
self.samples_by_class = defaultdict(list)
self.all_samples = []
self.unknown_samples = [] # 未知类别样本列表
self._load_dataset()
self._load_unknown_samples()
def _load_dataset(self):
"""加载数据集并按类别组织"""
@@ -191,24 +201,39 @@ class TripletDataset(Dataset):
logger.info(f"加载数据集完成:")
for class_name, class_idx in self.class_to_idx.items():
logger.info(f" {class_name}: {len(self.samples_by_class[class_idx])} 张图片")
def _load_unknown_samples(self):
"""加载未知类别样本"""
if not self.unknown_dir or not os.path.exists(self.unknown_dir):
logger.info("未指定未知类别目录或目录不存在,跳过未知样本加载")
return
# 遍历未知类别目录下的所有图片
for root, dirs, files in os.walk(self.unknown_dir):
for file in files:
if file.lower().endswith(('.png', '.jpg', '.jpeg')):
img_path = os.path.join(root, file)
self.unknown_samples.append(img_path)
logger.info(f"加载未知类别样本完成: {len(self.unknown_samples)} 张图片")
def __len__(self):
return len(self.all_samples)
def __getitem__(self, idx):
"""
获取三元组样本
Returns:
tuple: (anchor, positive, negative, anchor_label)
"""
# 获取锚点样本
anchor_path, anchor_label = self.all_samples[idx]
anchor_img = self._load_image(anchor_path)
# 获取正样本(同类别的不同图片)
# 需要从自己所在类别中先把自己给排除掉
positive_candidates = [path for path in self.samples_by_class[anchor_label]
positive_candidates = [path for path in self.samples_by_class[anchor_label]
if path != anchor_path]
if positive_candidates:
# 从这里可以看出来是随机选的
@@ -216,17 +241,42 @@ class TripletDataset(Dataset):
else:
positive_path = anchor_path # 如果只有一张图片,使用自己作为正样本
positive_img = self._load_image(positive_path)
# 获取负样本(不同类别的图片
# 获取负样本(使用策略选择未知类别或已知类别
negative_img = self._get_negative_sample()
return anchor_img, positive_img, negative_img, anchor_label
def _get_negative_sample(self):
"""
获取负样本,支持未知类别和已知类别的混合策略
Returns:
torch.Tensor: 负样本图片
"""
# 如果有未知类别样本且概率满足条件,优先选择未知类别
if self.unknown_samples and random.random() < self.unknown_prob:
# 选择未知类别作为负样本
negative_path = random.choice(self.unknown_samples)
return self._load_image(negative_path)
# 否则选择已知类别作为负样本(原来的逻辑)
# 为了安全起见,选择一个随机锚点
anchor_idx = random.randint(0, len(self.all_samples) - 1)
anchor_path, anchor_label = self.all_samples[anchor_idx]
# 获得其它类别
negative_classes = [cls for cls in self.samples_by_class.keys() if cls != anchor_label]
# 随便选一个类别
negative_class = random.choice(negative_classes)
# 随便选一个路径
negative_path = random.choice(self.samples_by_class[negative_class])
negative_img = self._load_image(negative_path)
return anchor_img, positive_img, negative_img, anchor_label
if negative_classes:
# 随便选一个类别
negative_class = random.choice(negative_classes)
# 随便选一个路径
negative_path = random.choice(self.samples_by_class[negative_class])
else:
# 如果没有其他类别,使用自己作为负样本(这种情况应该很少见)
negative_path = anchor_path
return self._load_image(negative_path)
def _load_image(self, image_path: str):
"""加载并预处理图片"""
@@ -644,13 +694,17 @@ def main(task_key: str = "dish"):
train_dataset = TripletDataset(
dataset_path=cfg.train_dir,
transform=transform_train,
samples_per_class=550
samples_per_class=550,
unknown_dir=cfg.unknown_dir,
unknown_prob=0.7 # 70%概率使用未知类别作为负样本
)
val_dataset = TripletDataset(
dataset_path=cfg.val_dir,
transform=transform_val,
samples_per_class=50
samples_per_class=50,
unknown_dir=cfg.unknown_dir,
unknown_prob=0.7 # 70%概率使用未知类别作为负样本
)
# 创建数据加载器
@@ -818,5 +872,5 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("task", choices=list(TASKS.keys()), nargs="?", default="dish")
args = parser.parse_args()
# main("whole_ingredient")
main("dish")
main("whole_ingredient")
# main("dish")