Python机器学习毕设实战:动漫角色识别,从数据生成、特征提取到MLP神经网络训练,带混淆矩阵、训练曲线可视化,附Tkinter图形识别界面及源码
“动漫角色识别”一听就很有画面感,其实就是让程序学会“看脸认人”:给它一张动漫头像,它能告诉你这是小樱、初音还是路飞。这个题目拿来当毕业设计或者练手项目都很合适,因为它把一个比较完整的机器学习流程——准备数据、提取特征、训练模型、评估、再做成界面——全都串起来了。
这有一些练手的项目,完整项目:https://pan.quark.cn/s/1e54aa2ae950
下面我就用大白话,把这个项目从零到一整个讲清楚,你跟着一步一步做,最后会有一个能跑、能出图、还能操作的可视化界面。
一、先想清楚整体思路
做这类“识别”任务,脑子里先要有一张流程图。我们这个项目的处理路径是:
- 准备数据:先有足够多的、按照角色分好类的图片;
- 提取特征:把每张图片变成一串数字(特征向量),让程序能够计算;
- 训练模型:用这些“数字”去训练一个神经网络,让它学会分辨不同角色;
- 评估效果:用没参与训练的图片去考,看它准不准,画出曲线和混淆矩阵;
- 落地识别:做个界面,让不认识的人也能一键测一张图。
一句话总结:先有数据,再变特征,然后学出一个分类器,最后把它接上界面供人使用。
二、用什么环境、装什么库
我用的是 Python 3,需要这几个库,功能在括号里标清楚了:
numpy:做批量数值计算;pillow:读写和缩放图片;scikit-learn:里面自带多层感知机神经网络(MLP),还带混淆矩阵、分类报告这些评估工具;matplotlib:画训练曲线、混淆矩阵、置信度条形图;joblib:把训练好的模型保存下来、再用时读出来;- 界面部分的
tkinter是 Python 自带的,不用另外装。
安装依赖这一步,在项目目录里输入下面的命令就行:
pip install -r requirements.txt
这里顺手提醒一下,代码里的中文显示都把字体指定成 simhei(黑体),这样在训练曲线、混淆矩阵的图上,标题和轴标签才不会变成一排小方框。你以后写自己的项目遇到图上中文乱码,多半也是字体没指定好的问题。
三、第一步:准备数据(generate_data.py)
真实的动漫数据集不太好找,而且下载慢、版权也麻烦。为了让项目能立刻跑起来、又能做成答辩,我选择**用程序自己生成一批“简笔动漫脸”**当演示数据,每个角色用不同的头发颜色、眼睛颜色和一个小标志区分。这样数据量稳定、特征也清晰,训练起来效果直观。
来看看这段生成数据的代码:
# -*- coding: utf-8 -*-
import os
import random
import argparse
import numpy as np
from PIL import Image, ImageDraw
random.seed(42)
np.random.seed(42)
IMG_SIZE = 112
# 角色标志 -> (名称, 头发颜色, 眼睛颜色, 前额装饰)
CHARACTERS = {
"sakura": ("小樱", (255, 160, 190), (200, 40, 90), "star"),
"sasuke": ("佐助", (30, 30, 70), (70, 70, 210), "drop"),
"luffy": ("路飞", (170, 120, 50), (90, 70, 40), "circle"),
"miku": ("初音", (100, 210, 230), (50, 200, 230), "s"),
"naruto": ("鸣人", (250, 205, 60), (80, 160, 220), "cap"),
"chihiro": ("千寻", (125, 40, 150), (120, 90, 60), "heart"),
}
def draw_face(key, seed=None):
hair_color, eye_color, mark = CHARACTERS[key][1], CHARACTERS[key][2], CHARACTERS[key][3]
rnd = random.Random(seed)
img = Image.new("RGB", (IMG_SIZE, IMG_SIZE), (250, 248, 245))
d = ImageDraw.Draw(img)
skin = (250, 236, 214)
dark = (40, 40, 45)
d.ellipse([16, 26, 96, 92], fill=hair_color) # 后发
d.ellipse([28, 44, 84, 96], fill=skin) # 脸
d.pieslice([18, 20, 96, 78], 180, 360, fill=hair_color) # 刘海
d.rectangle([18, 20, 96, 40], fill=hair_color)
d.pieslice([18, 20, 52, 88], 110, 250, fill=hair_color)
d.pieslice([64, 20, 98, 88], -70, 60, fill=hair_color)
for ex in (36, 76):
ey = int(62 + rnd.uniform(-2, 2))
d.ellipse([ex, ey, ex + 10, ey + 14], fill=eye_color)
d.ellipse([ex + 2, ey + 1, ex + 4, ey + 4], fill=(255, 255, 255))
d.line([32, 84, 38, 86], fill=dark, width=2)
# 前额标志,让每个角色更有个性
if mark == "star":
d.polygon([(25, 34), (29, 44), (18, 39), (32, 39), (21, 44)], fill=(255, 200, 60))
elif mark == "drop":
d.ellipse([26, 30, 34, 40], fill=(255, 120, 120))
elif mark == "circle":
d.ellipse([24, 28, 36, 40], outline=(255, 255, 255), width=2)
elif mark == "cap":
d.arc([22, 20, 60, 40], 100, 260, fill=(30, 30, 30), width=4)
elif mark == "heart":
d.polygon([(28, 34), (32, 40), (36, 34), (32, 44)], fill=(255, 120, 120))
return img
draw_face 这个函数就是“画一个卡通头像”:先画头发打底,再画一张脸、刘海、眼睛、嘴巴,最后在前额处加上一个小标志。不同角色换成不同的颜色和标志,就出现了天然的类别差异,这正是模型能学会分类的“依据”。
为了让数据量足够大、又不会全都一模一样,我还会在上面的基础上做一轮随机增强——把图片随机旋转、缩放、改变亮度。这样同一个角色的不同图片各有变化,网络学会的是“角色特征”,而不是“记图”。
def augment(img):
rnd = random.Random()
img = img.rotate(rnd.uniform(-8, 8), resample=Image.BICUBIC, fillcolor=(250, 248, 245))
zoom = rnd.uniform(0.9, 1.05)
nw = int(IMG_SIZE * zoom)
resized = img.resize((nw, nw), resample=Image.BICUBIC)
left = rnd.randint(0, max(0, nw - IMG_SIZE))
top = rnd.randint(0, max(0, nw - IMG_SIZE))
img = resized.crop((left, top, left + IMG_SIZE, top + IMG_SIZE))
arr = np.array(img).astype(np.float32) * rnd.uniform(0.85, 1.15)
np.clip(arr, 0, 255, out=arr)
return Image.fromarray(arr.astype(np.uint8))
最后把它按“训练集 / 测试集”分开存好,训练和测试各用各的,避免“自己考自己”的虚假高分:
def save_dataset(n_train, n_test):
for key, cdef in CHARACTERS.items():
name = cdef[0]
for split, count in (("train", n_train), ("test", n_test)):
d = os.path.join("data", split, name)
os.makedirs(d, exist_ok=True)
for i in range(count):
img = draw_face(key, seed=i * 1000 + 7)
if split == "train":
img = augment(img)
img.save(os.path.join(d, "%d.jpg" % i), "JPEG")
print("已生成 %s(%s)图像" % (name, key))
默认我给 6 个角色各生成 200 张训练图、60 张测试图,也就是训练集 1200 张、测试集 360 张。运行起来是这样:
python generate_data.py
四、第二步:统一提取特征(feature.py)
图片本身是一堆像素,直接丢给神经网络也行,但为了处理得快、效果稳,我先统一一下“格式”。
# -*- coding: utf-8 -*-
import numpy as np
from PIL import Image
IMG_SIZE = 32
def load_features(path):
img = Image.open(path).convert("RGB").resize((IMG_SIZE, IMG_SIZE))
arr = np.array(img, dtype=np.float32) / 255.0
return arr.reshape(-1)
def image_to_features(pil_img):
img = pil_img.convert("RGB").resize((IMG_SIZE, IMG_SIZE))
arr = np.array(img, dtype=np.float32) / 255.0
return arr.reshape(-1)
这段代码做的事很朴素,但很关键:
- 打开图片,统一转成 RGB;
- 统一缩放到 32×32 这样的一个小尺寸——图片小了,训练的速度就比较快;
- 把像素值除以 255,缩放到 0~1 之间,网络训练才稳定;
- 把二维像素“拉”成一维向量,
32×32×3 = 3072个数字,就是这个特征向量。
我把这一步抽成 feature.py,让训练、命令行识别、图形界面三方都调用它。这样“训练时怎么处理,识别时也怎么处理”,输入一致,模型才不会到处判。
五、第三步:训练模型(train.py)——带数据看效果
下面是最核心的一步。我用来识别的是一个多层感知机(MLP)神经网络:输入层接收上面那 3072 维特征,经过两个隐藏层(128 个神经元、64 个神经元)逐步提取规律,最后的输出层是 6 个数字,分别表示“是这 6 个角色各自的可能性”。
# -*- coding: utf-8 -*-
import os
import warnings
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.exceptions import ConvergenceWarning, UndefinedMetricWarning
import joblib
from feature import load_features
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
warnings.filterwarnings("ignore", category=ConvergenceWarning)
warnings.filterwarnings("ignore", category=UndefinedMetricWarning)
MODEL_PATH = "anime_model.job"
DEFAULT_EPOCHS = 20
def load_data(split):
X, y = [], []
root = os.path.join("data", split)
class_names = sorted([d for d in os.listdir(root)
if os.path.isdir(os.path.join(root, d))])
for name in class_names:
folder = os.path.join(root, name)
for fname in os.listdir(folder):
X.append(load_features(os.path.join(folder, fname)))
y.append(name)
return np.array(X, dtype=np.float32), np.array(y), class_names
load_data 把文件夹里的图片全部读进来,变成两个大数组:一个是特征矩阵 X,一个是对应的角色名 y,相当于把图片和正确答案一一对应。
然后开始“多轮地练”。我用的技巧是 warm_start=True 加上 max_iter=1,每一轮只迈一小步,然后记录一次当前准确率与损失,这样就能画出一条名副其实的“训练曲线”:
def train(epochs):
X_train, y_train, class_names = load_data("train")
X_test, y_test, _ = load_data("test")
print("训练集 %d 张,测试集 %d 张,角色类别 %d 个:%s"
% (len(X_train), len(X_test), len(class_names), class_names))
model = MLPClassifier(hidden_layer_sizes=(128, 64), activation="relu",
alpha=0.001, learning_rate_init=1e-3,
max_iter=1, warm_start=True, random_state=42)
train_acc, val_acc, losses = [], [], []
for epoch in range(epochs):
model.fit(X_train, y_train)
train_acc.append(model.score(X_train, y_train))
val_acc.append(model.score(X_test, y_test))
losses.append(model.loss_)
print("轮次 %02d/%d 训练准确率 %.4f 验证准确率 %.4f 损失 %.4f"
% (epoch + 1, epochs, train_acc[-1], val_acc[-1], model.loss_))
joblib.dump({"model": model, "classes": class_names}, MODEL_PATH)
pred = model.predict(X_test)
acc = float(np.mean(pred == y_test))
print("\n测试集整体准确率:%.2f%%" % (acc * 100))
print(classification_report(y_test, pred, target_names=class_names, digits=4))
plot_curves(train_acc, val_acc, losses)
plot_confusion(class_names, y_test, pred, acc)
你可能会问:为什么不把 fit 一次调到底、不看它中间的收敛过程?因为答辩员喜欢看“准确率和损失是怎么一步一步变化的”。所以我人为分轮,每轮记录三个关键数字:训练准确率、验证准确率、损失。
画曲线的两个函数很直观,一个画“准确率怎么升上去”,一个画“损失怎么降下来”:
def plot_curves(train_acc, val_acc, losses):
ep = range(1, len(train_acc) + 1)
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].plot(ep, train_acc, label="训练准确率")
axes[0].plot(ep, val_acc, label="验证准确率")
axes[0].set_title("准确率随训练轮数变化")
axes[0].set_xlabel("训练轮数"); axes[0].set_ylabel("准确率")
axes[0].legend()
axes[1].plot(ep, losses, label="训练损失", color="orange")
axes[1].set_title("损失随训练轮数变化")
axes[1].set_xlabel("训练轮数"); axes[1].set_ylabel("损失")
axes[1].legend()
plt.suptitle("动漫角色识别 · 训练过程", fontsize=14)
plt.tight_layout()
plt.savefig("accuracy_loss.png", dpi=100)
plt.close(fig)
然后是混淆矩阵。混淆矩阵是个 6×6 的表格,横轴是“预测的角色”,纵轴是“真实角色”,对角线越胖越好。答辩的时候讲“我每个角色都几乎没认错”,用这张图最有说服力:
def plot_confusion(class_names, y_true, pred, acc):
cm = confusion_matrix(y_true, pred)
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(cm, cmap="Blues")
ax.set_xticks(range(len(class_names)))
ax.set_yticks(range(len(class_names)))
ax.set_xticklabels(class_names)
ax.set_yticklabels(class_names)
ax.set_xlabel("预测角色")
ax.set_ylabel("真实角色")
ax.set_title("测试集混淆矩阵(整体准确率 %.2f%%)" % (acc * 100))
for i in range(len(class_names)):
for j in range(len(class_names)):
ax.text(j, i, cm[i, j], ha="center", va="center", color="black")
plt.colorbar(im)
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=100)
plt.close(fig)
运行方式:
python train.py
我实际跑出来的数据(答辩可以照抄)
我在默认参数下训练(每类 200 张训练图、60 张测试图,6 个角色),真实输出如下:
- 训练集:1200 张(6 类 × 200)
- 测试集:360 张(6 类 × 60)
- 最终测试集整体准确率:100.00%
- 训练过程中验证准确率第 9 轮就已经到 100%,之后一直稳定在 100%
- 训练损失从第 1 轮的 2.09 一路降到 0.02 附近,曲线非常典型
每一类角色的指标如下(precision 精确率、recall 查全率、f1-score 综合分、support 该类别样本数):
| 角色 | precision | recall | f1-score | 样本数 |
|---|---|---|---|---|
| 佐助 | 1.0000 | 1.0000 | 1.0000 | 60 |
| 初音 | 1.0000 | 1.0000 | 1.0000 | 60 |
| 千寻 | 1.0000 | 1.0000 | 1.0000 | 60 |
| 小樱 | 1.0000 | 1.0000 | 1.0000 | 60 |
| 路飞 | 1.0000 | 1.0000 | 1.0000 | 60 |
| 鸣人 | 1.0000 | 1.0000 | 1.0000 | 60 |
混淆矩阵(对角线全 60、其余全 0,说明没有认错):
[[60 0 0 0 0 0]
[ 0 60 0 0 0 0]
[ 0 0 60 0 0 0]
[ 0 0 0 60 0 0]
[ 0 0 0 0 60 0]
[ 0 0 0 0 0 60]]
这里要特别说明一下:因为我用的是程序生成的演示数据,颜色区分比较明显,所以准确率能到 100%。如果你换成真实动漫数据集,数字会降一些(比如 80~95%),那是正常的,反而更显得“真实”。答辩的时候这种分寸要讲清楚:demo 高准确率靠的是数据可区分,真实场景有难度才是常态。
六、第四步:命令行识别单张图(predict.py)
模型训完,就得“验收”。我写一个简单的命令行版,它从模型文件里把训练好的模型和角色顺序读出来,再给任意一张图,输出每个角色的置信度:
# -*- coding: utf-8 -*-
import sys
import joblib
from feature import load_features
def predict(img_path):
bag = joblib.load("anime_model.job")
model = bag["model"]
class_names = bag["classes"]
x = load_features(img_path).reshape(1, -1)
probs = model.predict_proba(x)[0]
order = sorted(range(len(probs)), key=lambda i: probs[i], reverse=True)
print("图片:%s" % img_path)
print("角色识别结果(按置信度从高到低):")
for i, idx in enumerate(order):
print(" %d. %s:%.2f%%" % (i + 1, class_names[idx], probs[idx] * 100))
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法:python predict.py 图片路径")
sys.exit(0)
predict(sys.argv[1])
输入(自己也可以挑张测试图片):
python predict.py data/test/小樱/0.jpg
实测输出如下(把一张小樱的测试图喂进去):
图片:data/test/小樱/0.jpg
角色识别结果(按置信度从高到低):
1. 小樱:99.94%
2. 初音:0.06%
3. 千寻:0.00%
4. 鸣人:0.00%
5. 路飞:0.00%
6. 佐助:0.00%
你看,程序非常硬地认出了小樱,置信度接近 100%。这就是前面积累的训练成果在“收获”了。
七、第五步:图形化界面(app.py)
命令行好用,但对不懂技术的人不友好。所以我又做了带窗口的图形界面(用 Python 自带的 Tkinter 来画控件,再用 matplotlib 在里面嵌一个条形图)。界面功能是:
- 点“选择图片”,从电脑里选一张动漫头像;
- 左边把这张图放大显示;
- 点“开始识别”;
- 右边即时画出一个每个角色置信度的条形图;
- 底部直接读出来识别结果。
核心代码长这样:
# -*- coding: utf-8 -*-
import os
import tkinter as tk
from tkinter import filedialog, messagebox
import numpy as np
import joblib
from PIL import Image, ImageTk
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from feature import image_to_features
FONT = "SimHei"
class AnimeApp:
def __init__(self):
self.model = None
self.class_names = []
self.image_path = None
self.photo = None
self.root = tk.Tk()
self.root.title("动漫角色识别系统")
self.root.geometry("900x580")
self.root.configure(bg="#f5f5f5")
tk.Label(self.root, text="动漫角色识别系统", font=(FONT, 20, "bold"),
bg="#f5f5f5", fg="#2a4d8f").pack(pady=12)
btns = tk.Frame(self.root, bg="#f5f5f5")
btns.pack()
tk.Button(btns, text="选择图片", font=(FONT, 13),
command=self.choose_image).pack(side="left", padx=10)
tk.Button(btns, text="开始识别", font=(FONT, 13),
command=self.recognize).pack(side="left", padx=10)
self.img_box = tk.Label(self.root,
text="尚未选择图片\n\n(点上方“选择图片”)",
font=(FONT, 12), bg="#ffffff", relief="groove")
self.img_box.pack(side="left", padx=25, pady=15)
self.fig = Figure(figsize=(5.2, 3.8), dpi=100)
self.fig.patch.set_facecolor("#f5f5f5")
self.ax = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
self.canvas.get_tk_widget().pack(side="right", padx=25, pady=15)
self.result_label = tk.Label(self.root, text="", font=(FONT, 16, "bold"),
bg="#f5f5f5", fg="#1c6b2c")
self.result_label.pack(side="bottom", pady=12)
其中“选图”和“识别”两个动作对应的函数如下。选图时读取并显示缩略图,识别时调用同一个 image_to_features 处理图片,再让模型预测,最后把概率画成条形图并给出结论:
def choose_image(self):
path = filedialog.askopenfilename(title="选择一张图片",
filetypes=[("图片", "*.jpg *.jpeg *.png"),
("所有文件", "*.*")])
if not path:
return
self.image_path = path
img = Image.open(path)
img.thumbnail((260, 320))
self.photo = ImageTk.PhotoImage(img)
self.img_box.configure(image=self.photo, text="", width=260, height=320)
self.result_label.configure(text="图片已选择,点“开始识别”即可。")
def recognize(self):
if not self.image_path:
messagebox.showwarning("提示", "请先选择一张图片。")
return
if not os.path.exists("anime_model.job"):
messagebox.showwarning("提示", "还没找到模型,请先运行训练脚本。")
return
if self.model is None:
bag = joblib.load("anime_model.job")
self.model = bag["model"]
self.class_names = bag["classes"]
x = image_to_features(Image.open(self.image_path)).reshape(1, -1)
probs = self.model.predict_proba(x)[0]
best = int(np.argmax(probs))
order = sorted(range(len(probs)), key=lambda i: probs[i], reverse=True)
self.ax.clear()
self.ax.barh([self.class_names[i] for i in order], probs[order], color="#2a6db5")
self.ax.set_title("置信度条形图", fontsize=12)
self.ax.set_xlim(0, 1)
self.ax.set_xlabel("置信度")
self.fig.tight_layout()
self.canvas.draw()
self.result_label.configure(text="识别结果:%s(置信度 %.2f%%)"
% (self.class_names[best], probs[best] * 100))
图上自带一个小标题“置信度条形图”,用横条的长短直观看谁的可能性更高。这样答辩的时候,台下一看就懂。
界面启动方式:
python app.py
提示:运行界面之前,要先跑一次 train.py 生成 anime_model.job 模型文件;如果还没有生成模型,界面会友好地提示你“先去训练”。
八、答辩时怎么讲这套项目更占理
答辩不只看“能不能跑”,关键在“你讲不讲得清”。给你思路:
- 数据从哪来?会不会作弊? —— 说明我用的是程序生成的演示数据,颜色区分明显,所以准确率高;换成真实数据集准确率会降到 80~95%,这是正常现象。
- 特征为什么先统一缩放到 32×32? —— 是为了速度,也为了让网络的输入维度固定;除以 255 是为了把像素值拉到 0~1,训练更稳定。
- 为什么用 MLP,不用卷积神经网络 CNN? —— 我先用最简单的 MLP 完整跑通流程,讲得清原理;多层卷积神经网络的实现更复杂、训练更久,但思想一脉相承:都是“特征 → 更深表示 → 分类”。你完全可以在总结里写“后续可升级为 CNN 提升泛化能力”。
- 混淆矩阵 / 损失曲线怎么解读? —— 横纵轴怎么对应、对角线越大越好、损失单调下降说明收敛正常。
- 界面怎么和模型接起来的? —— 重点在于模型被
joblib存成文件,界面通过同一个feature.py统一处理输入,所以“训练一套、识别同一套”。
九、最后总结
这个动漫角色识别项目,麻雀虽小五脏俱全:
- 用
PIL生成并增强图片数据; - 用
scikit-learn的多层感知机训练出可用的分类模型; - 用
matplotlib可视化出训练曲线、混淆矩阵和置信度条形图; - 用
tkinter做了一个有“图 + 数 + 结论”的图形化界面。
整个链路从数据到模型再到落地界面,每一步都能展开讲、都能量化展示,很适合在答辩现场现场演示。把这套思路写进你的毕设论文里,数据、图表、界面一应俱全,沟通起来就顺很多。
如果你想把准确率再拉起来、或者换更真实的角色数据集,方向也很明确:把 generate_data.py 里的图片换成真实照片,训练时多训几轮(比如 python train.py --epochs 40),体会一下网络在新数据上的表现。祝答辩顺利。
更多推荐
所有评论(0)