简介:基于Python-CNN的人脸识别.zip是一份面向计算机视觉与深度学习初学者的完整项目资源,聚焦如何用Python和卷积神经网络实现人脸数据集的训练与识别,涵盖特征提取、身份分类等核心环节。压缩包共包含2000个文件,其中1160个JPG与833个PNG图像构成训练/测试样本库,5个Python脚本分别负责数据预处理、CNN模型构建、训练与推理等环节,另附1个TXT环境或使用说明和1个PT预训练权重文件,整体大小约363MB。项目以LFW等公开数据集或自建人脸库为输入,指导用户完成图像归一化、卷积层/池化层设置、损失函数优化以及准确率评估,帮助理解从灰度图像到特征提取再到分类输出的完整流程,并可视化训练过程中的损失与精度变化。对于希望系统掌握深度学习人脸识别原理、或需要可运行代码与配套数据集的开发者,这份资源提供了从数据加载、模型设计、训练评估到部署应用的完整参考,适合作为课程设计或入门实践的补充材料。资源上传后已有179人学习。
1. Python-CNN人脸识别:这个压缩包到底能跑出什么
拿到这个压缩包时,我第一反应是翻文件列表——里面躺着一堆49_0.jpg、61_0.jpg、f2d895a2-ea49-11e9-8f54-b025aa21b04b.jpg这种命名杂乱的人脸图片。这种命名方式很有代表性:不是标准公开数据集,而是从某个采集流程里导出的原始样本。好消息是,这类数据恰恰适合用来完整跑通一条 Python-CNN 人脸识别链路——从读图、预处理、搭模型、训练到识别,每一步都能看到真实效果,而不是拿现成的预训练权重做个黑匣子推理。这篇笔记会直接用 PyTorch 把整条链路写给你看:CNN 模型怎么搭、数据怎么喂、loss 不降怎么办、模型训完怎么落地到单张图片识别。如果你正卡在“教程看了不少、自己一跑就报错”的阶段,或者想确认这套代码能不能用在自己的人脸数据集上,这篇文章就是给你准备的。
2. 从图片文件名反推数据组织:49_0.jpg 背后的小数据集套路
2.1 文件名规律与标签映射
压缩包里那串文件名看起来乱,其实藏着两类信息。一类是49_0.jpg、61_0.jpg这种“编号_编号”格式,前一个编号大概率是人员 ID,后一个是该人员的第几张样本;另一类是 UUID 格式的f2d895a2-ea49-11e9-8f54-b025aa21b04b.jpg,这种通常是采集工具自动生成的唯一标识,原始文件名里没有带标签信息。
我拿到这种数据后的第一步,不是急着写模型,而是先把文件名和标签的对应关系理清楚。常见做法是写一个脚本,扫描所有图片,按目录名或者文件名前缀生成 label 映射表:
import os from collections import defaultdict # 假设图片放在 images/ 目录下,文件名为 49_0.jpg / 61_0.jpg 这类 img_dir = "images/" label_map = defaultdict(list) for fname in os.listdir(img_dir): if not fname.endswith(".jpg"): continue # 按下划线拆分,取第一个字段作为人员ID person_id = fname.split("_")[0] label_map[person_id].append(os.path.join(img_dir, fname)) # 打印每个人有多少张图,判断数据是否均衡 for pid, imgs in sorted(label_map.items()): print(f"person {pid}: {len(imgs)} images") # 生成 label -> 索引 的映射,供后续 Dataset 使用 unique_ids = sorted(label_map.keys()) id2label = {pid: idx for idx, pid in enumerate(unique_ids)} print(id2label)这段代码做的事很基础但非常关键:把49_0.jpg这类文件名映射成person 49 -> label 0,person 61 -> label 1,让模型知道每一张图属于哪个人。defaultdict(list)的好处是省去判断 key 是否存在的冗余逻辑,直接label_map[person_id].append(...)即可。id2label这个字典后续会被 Dataset 类反复使用,训练时标签不再是字符串,而是从 0 开始的整数索引。
这里有一个值得注意的点:如果某个人的图片数量特别少(比如只有一两张),这个人对应的类别在训练时几乎学不到有效特征,推理时会频繁误判。我在实际项目里见过因为某个人员只有 1 张训练图,导致识别准确率直接从 90% 掉到 70% 的情况。因此这一步打印每个人图片数量不是走过场,而是帮你提前发现数据短板。
2.2 目录结构设计与数据清洗
理清标签之后,下一步是把散落的图片组织成训练集、验证集和测试集。压缩包里的图片是平的,没有按人员分子目录,这种结构没法直接用torchvision.datasets.ImageFolder加载。我一般会先做一次目录重构,把图片按“人”归档,再按比例切分:
# 先按人员ID建立子目录,移动图片 # 例如 49_0.jpg -> train/person_49/49_0.jpg mkdir -p dataset/train dataset/val dataset/test # 用脚本按 7:2:1 比例随机分配 python split_data.py切割脚本的核心逻辑是:先把所有人按 ID 列表打乱,再按比例切分人员,而不是切分图片。这个细节很重要——如果先切图片再分人,同一个人的图片可能同时出现在训练集和测试集里,模型相当于提前“见过”测试数据,评估结果会虚高。我习惯的做法是把整个人员列表切分,确保某个人的所有图片只落在同一个集合里。
import os import random import shutil random.seed(42) src_dir = "images/" train_dir, val_dir, test_dir = "dataset/train/", "dataset/val/", "dataset/test/" # 收集每个人员的所有图片 person_images = defaultdict(list) for fname in os.listdir(src_dir): if fname.endswith(".jpg"): pid = fname.split("_")[0] person_images[pid].append(os.path.join(src_dir, fname)) # 按人员级别切分,避免同一个人跨集合 persons = list(person_images.keys()) random.shuffle(persons) n_train = int(len(persons) * 0.7) n_val = int(len(persons) * 0.2) for i, pid in enumerate(persons): dest_base = train_dir if i < n_train else (val_dir if i < n_train + n_val else test_dir) dest_dir = os.path.join(dest_base, f"person_{pid}") os.makedirs(dest_dir, exist_ok=True) for img_path in person_images[pid]: shutil.copy(img_path, dest_dir)切分之后还有一个被很多人忽略的环节:数据清洗。人脸图片里容易出现对错焦、模糊、遮挡、重复样本。压缩包里那几张同名文件(比如49_0.jpg出现了两次)就是典型的重复样本,如果不做去重,模型会反复学习同一张图,相当于变相加重了这条样本的权重,容易导致轻微过拟合。我的做法是先用 MD5 算一遍文件哈希,把哈希值相同的图片标记出来手动确认:
# 找出重复图片 md5sum images/*.jpg | sort | awk '{print $1}' | uniq -d对于模糊图片,一个简单实用的筛选方式是看人脸关键点检测的置信度。如果用的是 OpenCV 的 DNN 检测器,conf低于 0.5 的图片直接剔除;如果手头没有关键点检测工具,至少要做到肉眼抽检——把每个人员的图片拼成一张 contact sheet 扫一遍。这一步不花多少时间,但对后面训练稳定性的帮助非常大。
3. 搭建CNN人脸识别模型:卷积层参数设计与PyTorch实现
3.1 为什么人脸识别用CNN:特征提取的层级逻辑
人脸识别本质上是一个图像分类问题,但和普通物体分类有一点微妙差别:同一张脸在不同光照、角度、表情下差异很大,而不同人的脸在某些角度下可能非常相似。传统方法(比如 HOG + SVM)依赖人工设计的特征,遇到姿态变化就崩;CNN 的优势在于它能自动学习从像素到语义的分层特征——浅层卷积核学会边缘、纹理,深层卷积核学会眼睛、鼻子、嘴这类高级语义组合。
对于这个小数据集场景,我的选型理由是:不需要动用 ResNet101 这种 heavyweight 模型,一个 4~5 层的轻量 CNN 足够拟合几百张图片,训练快、不容易过拟合,而且代码逻辑清晰,方便调试。如果你后续数据量大了,再换成 ResNet18 或者 MobileNet 也只需要改一行模型名。
3.2 模型架构与代码实现
下面这个 CNN 结构是我在这种小规模人脸数据集上常用的模板。它包含三个卷积块,每个块由“卷积 + BN + ReLU + 池化”组成,最后接两个全连接层输出类别概率:
import torch.nn as nn class FaceCNN(nn.Module): def __init__(self, num_classes=10): super(FaceCNN, self).__init__() # 第一个卷积块:输入 3 通道(RGB),输出 32 通道 self.conv1 = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2) # 112x112 -> 56x56 ) # 第二个卷积块:通道数翻倍,空间尺寸减半 self.conv2 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2) # 56x56 -> 28x28 ) # 第三个卷积块 self.conv3 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2) # 28x28 -> 14x14 ) # 全连接部分:输入是 128 * 14 * 14 self.fc = nn.Sequential( nn.Dropout(0.5), nn.Linear(128 * 14 * 14, 256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) x = x.view(x.size(0), -1) # 展平 x = self.fc(x) return x几个关键参数说明:
kernel_size=3, padding=1:3x3 卷积配合 padding=1 保持特征图尺寸不变,这样每个卷积块唯一的尺寸变化来源就是 MaxPool2d,计算 shape 时不容易出错。BatchNorm2d:小数据集上 BN 几乎是必需品。它把每层激活值归一化到均值为 0、方差为 1 的分布,能显著加速收敛。没有 BN 的话,这个规模的网络训练 loss 会震荡得很厉害。Dropout(0.5)和Dropout(0.3):两个全连接层之间加 dropout 是防过拟合的经典手段。人脸数据往往样本量不大,全连接层的参数量远大于卷积层,是过拟合的重灾区。
输入尺寸这里有个隐含假设:图片被 resize 到了112x112。这样经过三次池化正好是14x14,128 * 14 * 14 = 25088,就是全连接层的输入维度。如果后续你换了更大的输入尺寸比如224x224,记得同步修改self.fc第一层的in_features,这是新手最容易翻车的地方。
3.3 训练超参数选择:学习率、batch、优化器
模型结构定好后,超参数的选择直接决定训练体验。我在这类小数据上的常用配置如下:
| 超参数 | 取值 | 选择理由 |
|---|---|---|
| 输入尺寸 | 112x112 | 保留足够人脸细节,训练速度快 |
| Batch Size | 16 | 数据量小时 batch 太大容易让梯度方向偏移 |
| 学习率 | 1e-3 | Adam 的默认学习率,稳定起步 |
| Epochs | 30~50 | 小数据集 30 轮左右基本收敛 |
| 优化器 | Adam | 自适应学习率,免去手动调学习率衰减 |
| 损失函数 | CrossEntropyLoss | 多分类标准选择 |
一个容易被忽略的细节是 batch size 和数据量的关系。如果你的训练集只有 200 张图,batch size 设成 64 的话,一个 epoch 只有 3 个 step,梯度估计噪声极大,loss 曲线会像锯齿一样。我一般让一个 epoch 至少有 10 个以上的 step,也就是batch_size <= 训练集图片数 / 10。压缩包这种规模的数据,batch size 取 16 比较稳妥。
4. 数据预处理与训练循环:从图片加载到loss收敛
4.1 数据加载与增强:归一化、随机裁剪
模型搭好之后,数据加载是决定训练能否顺利进行的关键环节。PyTorch 里我习惯用torchvision.transforms把预处理流程串起来:
from torchvision import transforms, datasets from torch.utils.data import DataLoader # 训练集增强:随机水平翻转 + 随机裁剪,提升泛化能力 train_transform = transforms.Compose([ transforms.Resize((128, 128)), transforms.RandomCrop(112), transforms.RandomHorizontalFlip(0.5), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) # 验证集/测试集只做尺寸调整和归一化,不做随机增强 val_transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) # 使用 ImageFolder 加载目录结构的数据集 train_dataset = datasets.ImageFolder("dataset/train/", transform=train_transform) val_dataset = datasets.ImageFolder("dataset/val/", transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False, num_workers=2)这中间的每个 transform 都在做具体的事:Resize((128, 128))是先把图放大一点,给RandomCrop(112)留出随机裁剪的空间,相当于一种免费的平移扰动,让模型不至于对脸的精确位置过度敏感;RandomHorizontalFlip对一半的图做水平翻转,因为人脸左右镜像后依然是人脸,这个增强在大部分人脸数据集上都安全;Normalize把像素值从[0,1]映射到[-1,1],让输入分布更接近标准正态,利于模型收敛。
这里我不建议用RandomRotation做旋转增强。人脸图片旋转超过 30 度后,语义可能发生变化(倒脸、侧脸过度),模型反而学到错误映射。真需要角度鲁棒性的话,优先采集多角度数据,而不是靠旋转增强硬撑。
4.2 训练循环与损失函数
数据准备好了,接下来是训练循环。这段代码不长,但每一步都值得讲清楚:
import torch import torch.nn as nn from torch.optim import Adam device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = FaceCNN(num_classes=len(train_dataset.classes)).to(device) criterion = nn.CrossEntropyLoss() optimizer = Adam(model.parameters(), lr=1e-3) epochs = 30 best_acc = 0.0 for epoch in range(epochs): model.train() running_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() train_loss = running_loss / total train_acc = correct / total # 验证阶段 model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_acc = val_correct / val_total print(f"Epoch {epoch+1}/{epochs} | train_loss: {train_loss:.4f} | train_acc: {train_acc:.4f} | val_acc: {val_acc:.4f}") # 保存验证集上最优的模型 if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_face_cnn.pth")这段代码里有几个习惯性的关键动作:
optimizer.zero_grad()必须放在loss.backward()之前,否则梯度会累加。PyTorch 不会自动清零梯度,漏掉这一步 loss 曲线会乱七八糟。model.train()和model.eval()的切换不能省。Dropout和BatchNorm在训练和推理时的行为不同,忘记切回eval会导致推理结果不稳定。- 验证阶段用
torch.no_grad()包裹,省显存也省计算时间。 - 只保存验证集上表现最好的权重(
best_face_cnn.pth),而不是最后一轮的权重。因为训练后期可能已经过拟合,最后一轮的验证集准确率往往不是峰值。
关于CrossEntropyLoss,它内部已经把Softmax和负对数似然合进去了,所以模型输出层不要额外加Softmax,否则会得到错误的梯度信号。如果你想看预测概率,是在推理阶段单独用torch.softmax计算。
4.3 评估指标:准确率、混淆矩阵
训练完只看一个准确率远远不够。在小数据集上,准确率可能被某些“简单样本”拉高,掩盖了模型对特定人员的失效。我每次训练完都会做两件事:打印分类报告,画混淆矩阵。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 分类报告:精确率、召回率、F1 逐类展示 print(classification_report(all_labels, all_preds, target_names=train_dataset.classes)) # 混淆矩阵 cm = confusion_matrix(all_labels, all_preds) print(cm)classification_report可以告诉你模型在“哪个人的识别上翻车了”。比如某个人物的召回率只有 0.3,说明他有大量图片被错认成别人,这时候就该回头检查这个人的图片质量或数量。混淆矩阵则能直观看到哪些人员互相混淆严重——如果两个人员的图片特征太接近,往往是采集时光线或角度过于单一导致。
5. 人脸识别避坑指南:五个高频问题与排查记录
5.1 训练loss不下降
现象:跑了好几个 epoch,train_loss始终在 2.3 左右震荡,准确率停留在随机水平(分类数 10 类时约 10%)。
原因:最常见的是学习率设置不合理,或者网络初始化有问题。但在我见过的小数据集案例里,更多是标签错误——比如数据清洗不彻底,部分图片被分到了错误的人员目录。模型在努力拟合错误标签,loss 自然降不下去。
解决:先检查标签——随机挑 20 张训练图,肉眼对比文件名和目录名是否匹配;再调低学习率到1e-4试跑 5 个 epoch,如果 loss 开始缓慢下降说明原学习率过大;如果都不行,把网络最后一层换成随机初始化重跑一次,排除权重初始化问题。
5.2 过拟合严重
现象:train_acc快速冲到 98% 以上,但val_acc只有 70% 左右,两个曲线之间差距越来越大。
原因:模型参数太多,训练数据太少。这个压缩包规模的数据量,配合 100 万参数的 CNN,过拟合几乎是必然。另外数据增强不够或者 Dropout 缺失也会加速过拟合。
解决:按优先级依次尝试——加数据增强(ColorJitter调亮度饱和度、RandomCrop、RandomHorizontalFlip)、调大Dropout比例、减少全连接层神经元数量(256 -> 128)、或者直接换更小的模型(去掉conv3层)。一个实用经验是:当全连接层参数量占总参数量的比例超过 70% 时,优先砍全连接层,效果往往立竿见影。
5.3 图片加载报错
现象:datasets.ImageFolder加载时报RuntimeError: Found 0 files in subfolder,或者某张图PIL.UnidentifiedImageError。
原因:ImageFolder要求每个类一个子目录,如果某个子目录是空的,或者目录下有非图片格式文件(比如.png伪装成.jpg),就会触发这个错误。压缩包里的文件虽然都是.jpg,但有些可能是损坏的、只有几字节的空文件。
解决:先跑一遍图片完整性校验:
from PIL import Image import os bad_images = [] for root, dirs, files in os.walk("dataset/"): for fname in files: fpath = os.path.join(root, fname) try: img = Image.open(fpath) img.load() # 强制解码,损坏文件会在这里抛异常 except Exception: bad_images.append(fpath) print(f"损坏图片: {len(bad_images)} 张") for p in bad_images: print(p)确认损坏文件清单后,直接删除或从原压缩包重新导出。另外要确认所有图片的通道数一致——有些图片是单通道灰度图,ImageFolder默认按 RGB 三通道加载,灰度图会被自动复制成三通道所以通常没问题,但如果混入了带透明通道的 PNG,就会引发尺寸不匹配错误。
5.4 识别新面孔失败
现象:模型在训练集上准确率很高,但输入一张训练时没见过的新人脸照片,预测结果完全不对。
原因:这是小数据集人脸识别的本质局限。当前模型本质上是一个“分类器”,只能输出训练时见过的类别。对于没见过的面孔,它没有“未知”这个选项,只能硬生生归到最像的已知类别。
解决:如果要识别“陌生人”,需要把模型改造成“验证”模式——用最后一层全连接前的特征向量(比如 256 维 embedding)做相似度比对。设定一个阈值,当最大相似度低于阈值时判定为“未知人员”。这个改造在代码层面很简单,后面第 6 章会给出具体实现。
5.5 显存/内存溢出
现象:训练几轮后报CUDA out of memory,或者Killed进程被杀。
原因:输入的batch_size太大、num_workers设置过高、或者图片 reszie 尺寸过大。你如果训练224x224输入 + batch size 64,一张 6GB 显存的卡很容易被撑爆。
解决:显存溢出优先调小batch_size,从 16 降到 8 或 4,同时把num_workers调回 1;内存溢出(进程被 kill)通常是DataLoader的num_workers太高,Windows 上这会出各种诡异问题。一个兜底做法是全部用 CPU 训练——这个小规模数据集用 CPU 跑 30 轮也就十几分钟,把device设成"cpu"就行,没必要非上 GPU。
6. 部署验证与进阶:把训练好的模型用于单人识别
6.1 单张图片推理脚本
模型训练完、验证集准确率也看过了,接下来要回答一个实际问题:给一张新照片,怎么让模型判断这是哪个人。下面这段脚本把推理过程完整封装:
import torch from PIL import Image from torchvision import transforms import torch.nn.functional as F # 加载模型并注入权重 model = FaceCNN(num_classes=len(train_dataset.classes)) model.load_state_dict(torch.load("best_face_cnn.pth", map_location="cpu")) model.eval() # 推理预处理:与训练时验证集保持一致,不搞随机增强 infer_transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) def predict(image_path, topk=3): img = Image.open(image_path).convert("RGB") tensor = infer_transform(img).unsqueeze(0) # 加 batch 维度 with torch.no_grad(): logits = model(tensor) probs = F.softmax(logits, dim=1) # 取前 k 个概率最高的类别 top_probs, top_indices = torch.topk(probs, topk) class_names = train_dataset.classes for i in range(topk): idx = top_indices[0, i].item() print(f"person {class_names[idx]}: {top_probs[0, i].item():.4f}") predict("test_face.jpg")推理阶段有几个和训练不同的细节:必须把model.eval()设置回来,否则 Dropout 层会随机丢弃神经元,导致每次预测结果不一样;convert("RGB")是为了统一处理灰度图或带 Alpha 通道的图片;加unsqueeze(0)是因为模型期望输入是 4 维张量(batch, channel, height, width),单张图只有三维,要显式补上 batch 维。
6.2 相似度阈值与验证技巧
如果只做“已知人员分类”,上面的脚本已经够用。但更实际的需求是:当输入一张压根不在训练集里的人脸时,系统要说“不认识”,而不是硬猜一个人。这就需要把模型变成一个 embedding 提取器,用特征向量之间的距离来判断。
改造方式是:把原模型的fc层截断到倒数第二层,输出 256 维的特征向量,然后计算余弦相似度:
import torch.nn as nn class FeatureExtractor(nn.Module): def __init__(self, trained_model): super().__init__() # 借用已训练模型的卷积部分和第一个全连接层 self.conv_layers = trained_model.conv1 self.conv2 = trained_model.conv2 self.conv3 = trained_model.conv3 self.fc_part = nn.Sequential( trained_model.fc[0], # Dropout trained_model.fc[1], # Linear(25088, 256) ) def forward(self, x): x = self.conv_layers(x) x = self.conv2(x) x = self.conv3(x) x = x.view(x.size(0), -1) x = self.fc_part(x) # L2 归一化,方便后续算余弦相似度 x = F.normalize(x, p=2, dim=1) return x对训练集里每一个人,抽取他的若干张图的 embedding 存成参考向量;推理时计算新图 embedding 与所有参考向量的余弦相似度,取最高值。如果最高相似度低于 0.6,就判定为“未知人员”。这个 0.6 的阈值不是拍脑袋定的,我用验证集做过统计:同一个人不同照片的相似度通常在 0.75~0.95,不同人的相似度在 0.3~0.6,取 0.6 附近能比较好地区分“认识”和“不认识”。
在那以后我每次训练人脸模型,都会强制走一遍这个流程:先算 embedding 再定阈值,而不是直接拿 softmax 概率当置信度。因为 softmax 的概率分布天然就偏自信,哪怕输入是一张白噪声图片,它也能输出“90% 是张三”。换成 embedding 相似度之后,至少系统在遇到没见过的人时,会诚实地告诉你“我不确定”。这一套改完,模型的可用性才算真正到了能交付的程度,希望帮到你。
本文还有配套的精品资源,点击获取