【通用视觉检测】训练Qwen3-vl-2B鉴别图片
·
需求背景:给你一张自拍照片,检测这个照片是否符合证件照的需求,具体需求如下:
| 特征名称 | 判断标准 |
|---|---|
| pure_color | 背景颜色是否为纯色,均匀的颜色返回1,有杂物或背景有多种颜色返回0 |
| background_color | 背景颜色,纯白色返回1,否则0。详细要求:比rgb(202,202,194)更白返回1 |
| sit_upright | 坐姿端正,,符合返回1,否则0。侧面照、扭头、斜视返回0 |
| head_obscured | 头部被物品遮挡(如刘海、帽子等),遮挡返回1,否则0 |
| face_obs | 面部被遮挡(如口罩、手指等),遮挡返回1,否则0 |
| face_shadow | 脸上有阴影(光照不好),有返回1,否则0 |
| face_spots | 脸上是否有白色斑点,有返回1,否则0 |
| eye_focus | 目光是否正视正前方,不能从侧面拍,符合返回1,否则0。 |
| eye_close | 眼睛是否闭上,,有一只闭上就返回1,否则0 |
| haireye | 头发是否遮住眉毛,,有返回1,否则0 |
| glass | 是否佩戴有颜色的墨镜(如蓝色或黑色墨镜),有返回1,否则0。 |
| glassspot | 佩戴的眼镜是否反光,反光返回1,否则0 |

需求分析:显然这不是简单的检测,需要一定的图片理解。尤其是坐姿端正,这种标签。
第一步:生成训练数据,我们使用qwen3-vl-235b-a22b-thinking 作为教师模型,进行模型打标。得到训练数据集的格式如下:
{
"messages": [
{
"role": "user",
"content": "你是一位摄影师,擅长拍摄证件照,请仔细判断下面这个证件照:<image>,判断这个照片是否符合以下要求:pure_color(背景颜色是否为纯色,均匀的颜色就返回1,有杂物或者背景颜色有多种颜色就返回0), background_color(背景颜色,如果纯白色返回 1,否则 0。例如:比rgb(202,202,194)更白返回1,比rgb(178,178,168)更黑就返回0。),sit_upright(符合下面所有要求,坐姿端正,人物的脸部完全100%正对镜头,头部必须完全正直,不能歪脖子,是就返回1 ,否返回0 ,例如侧面照,扭头,斜视返回0),head_obscured(头部被物品遮挡,比如刘海,帽子等,遮挡就返回1,否则0),face_obs(面部被遮挡,比如口罩,手指等,遮挡就返回1,否则0), face_shadow(脸上有阴影,就是光照不好,如有返回 1,否则0),face_spots(脸上是否有白色的斑点,如果有就返回1,否则返回0)。eye_focus(这个证件照,目光有没有正视正前方,符合证件照的严格要求,拍照的角度正对着脸,不能从侧面拍,符合就返回1,否则返回0。 注意出现下面的情况返回0:脖子歪了,瞳孔并没有完全居中,眼神向左,眼神向右,视线并没有完全平视正前方镜头),eye_close(眼睛是否闭上,没睁开,留个缝也算闭上,有一个闭上就是1,否则0),haireye(判断头发有没有遮住眉毛,只要有一根头发遮住眉毛就是不合格,有就返回1,否则0),glass(是否佩戴有颜色的墨镜,比如蓝色或者黑色的模型,有就返回1,否则0。如果是透明的镜片返回0),glassspot(佩戴的眼镜是否反光,反光就返回1,否则0)。用json格式返回.只返回json格式,不返回其他内容。"
},
{
"role": "assistant",
"content": {
"pure_color": 1,
"background_color": 1,
"sit_upright": 1,
"head_obscured": 0,
"face_obs": 0,
"face_shadow": 0,
"face_spots": 0,
"eye_focus": 1,
"eye_close": 0,
"haireye": 0,
"glass": 0,
"glassspot": 0
}
}
],
"images": [
"./pics/1.jpg"
]
}
第二步模型训练:
使用ms-swift框架,需要48GB显存,进行full训练,不建议用lora训练。:
训练代码如下:
PYTORCH_CUDA_ALLOC_CONF='expandable_segments:True' \
IMAGE_MAX_TOKEN_NUM=1024 \
CUDA_VISIBLE_DEVICES=0 \
swift sft \
--model ./Qwen/Qwen3-VL-2B-Instruct \
--dataset train2.jsonl \
--load_from_cache_file true \
--split_dataset_ratio 0.01 \
--tuner_type full \
--torch_dtype bfloat16 \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--learning_rate 1e-4 \
--lora_rank 8 \
--lora_alpha 32 \
--target_modules all-linear \
--freeze_vit true \
--freeze_aligner true \
--gradient_checkpointing true \
--vit_gradient_checkpointing false \
--gradient_accumulation_steps 2 \
--eval_steps 100 \
--save_steps 100 \
--save_total_limit 2 \
--logging_steps 5 \
--max_length 4096 \
--output_dir output \
--warmup_ratio 0.05 \
--deepspeed zero2 \
--dataset_num_proc 4 \
--dataloader_num_workers 4
这里我们使用freeze_vit trye/false进行对比,测试一下效果。
最后得到的模型,我们进行本地推理:
import json
import os
import ast
import requests
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from PIL import ImageColor
import base64
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
additional_colors = [colorname for (colorname, colorcode) in ImageColor.colormap.items()]
def vl_inference(img_url, prompt):
with open(img_url, "rb") as image_file:
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"image": f"data:image/jpeg;base64,{base64_image}",
},
{"type": "text", "text": prompt},
],
}
]
# Preparation for inference
inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt"
)
inputs = inputs.to(model.device)
# Inference: Generation of the output
generated_ids = model.generate(**inputs, max_new_tokens=512)
generated_ids_trimmed = [
out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
output_text = processor.batch_decode(
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)
return output_text[0]
if __name__ == '__main__':
model = Qwen3VLForConditionalGeneration.from_pretrained(
"./qwen3vl2bvitfalse", dtype="auto", device_map="auto"
)
processor = AutoProcessor.from_pretrained("./qwen3vl2bvitfalse")
prompt = '你是一位摄影师,擅长拍摄证件照,请仔细判断下面这个证件照,判断这个照片是否符合以下要求:pure_color(背景颜色是否为纯色,均匀的颜色就返回1,有杂物或者背景颜色有多种颜色就返回0), background_color(背景颜色,如果纯白色返回 1,否则 0。例如:比rgb(202,202,194)更白返回1,比rgb(178,178,168)更黑就返回0。),sit_upright(符合下面所有要求,坐姿端正,人物的脸部完全100%正对镜头,头部必须完全正直,不能歪脖子,是就返回1 ,否返回0 ,例如侧面照,扭头,斜视返回0),head_obscured(头部被物品遮挡,比如刘海,帽子等,遮挡就返回1,否则0),face_obs(面部被遮挡,比如口罩,手指等,遮挡就返回1,否则0), face_shadow(脸上有阴影,就是光照不好,如有返回 1,否则0),face_spots(脸上是否有白色的斑点,如果有就返回1,否则返回0)。eye_focus(这个证件照,目光有没有正视正前方,符合证件照的严格要求,拍照的角度正对着脸,不能从侧面拍,符合就返回1,否则返回0。 注意出现下面的情况返回0:脖子歪了,瞳孔并没有完全居中,眼神向左,眼神向右,视线并没有完全平视正前方镜头),eye_close(眼睛是否闭上,没睁开,留个缝也算闭上,有一个闭上就是1,否则0),haireye(判断头发有没有遮住眉毛,只要有一根头发遮住眉毛就是不合格,有就返回1,否则0),glass(是否佩戴有颜色的墨镜,比如蓝色或者黑色的模型,有就返回1,否则0。如果是透明的镜片返回0),glassspot(佩戴的眼镜是否反光,反光就返回1,否则0)。用json格式返回.只返回json格式,不返回其他内容。'
dir_path = '../pics'
dlist = []
for filename in os.listdir(dir_path):
if filename.endswith('.jpg'):
img_path = os.path.join(dir_path, filename)
print(img_path)
content_text = vl_inference(img_path, prompt)
print(content_text)
dlist.append([img_path, content_text])
with open("./eval_local_2b_vitfalse.jsonl", "w", encoding="utf-8") as f:
for item in dlist:
# ensure_ascii=False 可以防止中文被转义为 \uXXXX
f.write(json.dumps(item, ensure_ascii=False) + "\n")
然后我们把结果进行对比:
| 指标 | full | full_vit 解冻 | 2b (原始模型) |
|---|---|---|---|
| background_color | 90.78% | 56.68% | 38.25% |
| pure_color | 97.70% | 82.49% | 83.87% |
| eye_focus | 97.24% | 92.17% | 92.17% |
| glass | 100.00% | 100.00% | 98.16% |
| eye_close | 98.62% | 95.85% | 97.70% |
| face_shadow | 99.08% | 99.08% | 98.62% |
| face_obs | 98.16% | 96.31% | 97.70% |
| glassspot | 96.31% | 96.31% | 97.70% |
| face_spots | 97.24% | 97.24% | 97.24% |
| sit_upright | 94.93% | 95.85% | 95.85% |
| haireye | 90.78% | 90.78% | 90.78% |
| head_obscured | 100% | 81.11% | 81.11% |
结果分析:background_color,haireye的准确率不高,主要是业务方的定义,白色并不是要求纯白色,而是模拟真实拍照场景的白色,有一定的灰度,这个灰度不好控制,模型容易误判。需要扩大训练数据集。
另外vit 冻结训练效果更好一些。
量化后,训练后的模型可以实现0.2秒出结果,只占用4GB显存,适合端侧部署。
更多推荐
所有评论(0)