added uncertain training

This commit is contained in:
Linrador 2026-05-06 21:32:05 +02:00
parent 2d5e522475
commit cb7f32d24d
2 changed files with 247 additions and 9 deletions

View File

@ -443,6 +443,9 @@ func trainingNextHandler(w http.ResponseWriter, r *http.Request) {
forceNew := r.URL.Query().Get("force") == "1" || forceNew := r.URL.Query().Get("force") == "1" ||
strings.EqualFold(r.URL.Query().Get("force"), "true") strings.EqualFold(r.URL.Query().Get("force"), "true")
preferUncertain := strings.EqualFold(r.URL.Query().Get("mode"), "uncertain") ||
strings.EqualFold(r.URL.Query().Get("sampleMode"), "uncertain")
root, err := trainingRootDir() root, err := trainingRootDir()
if err != nil { if err != nil {
trainingWriteError(w, http.StatusInternalServerError, err.Error()) trainingWriteError(w, http.StatusInternalServerError, err.Error())
@ -452,7 +455,7 @@ func trainingNextHandler(w http.ResponseWriter, r *http.Request) {
refreshPrediction := r.URL.Query().Get("refresh") == "1" || refreshPrediction := r.URL.Query().Get("refresh") == "1" ||
strings.EqualFold(r.URL.Query().Get("refresh"), "true") strings.EqualFold(r.URL.Query().Get("refresh"), "true")
if !forceNew { if !forceNew && !preferUncertain {
var startedAtMs int64 var startedAtMs int64
if refreshPrediction { if refreshPrediction {
@ -476,9 +479,22 @@ func trainingNextHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
startedAtMs := publishAnalysisStarted("", 4, "Neues Trainingsbild wird vorbereitet…") startedAtMs := publishAnalysisStarted("", 4, func() string {
if preferUncertain {
return "Unsichere Prediction wird gesucht…"
}
return "Neues Trainingsbild wird vorbereitet…"
}())
var sample *TrainingSample
if preferUncertain {
sample, err = trainingCreateUncertainNextSampleWithProgress(startedAtMs)
} else {
sample, err = trainingCreateNextSampleWithProgress(startedAtMs)
}
sample, err := trainingCreateNextSampleWithProgress(startedAtMs)
if err != nil { if err != nil {
publishAnalysisError(startedAtMs, "", "Trainingsbild konnte nicht erstellt werden.", err) publishAnalysisError(startedAtMs, "", "Trainingsbild konnte nicht erstellt werden.", err)
trainingWriteError(w, http.StatusInternalServerError, err.Error()) trainingWriteError(w, http.StatusInternalServerError, err.Error())
@ -1824,6 +1840,153 @@ func trainingCreateNextSample() (*TrainingSample, error) {
return sample, nil return sample, nil
} }
func trainingCreateUncertainNextSampleWithProgress(startedAtMs int64) (*TrainingSample, error) {
const attempts = 8
root, err := trainingRootDir()
if err != nil {
return nil, err
}
var best *TrainingSample
bestScore := -1.0
errs := []string{}
for i := 0; i < attempts; i++ {
publishAnalysisStep(
startedAtMs,
i+1,
attempts+1,
"",
fmt.Sprintf("Unsicherer Kandidat %d/%d wird analysiert…", i+1, attempts),
)
sample, err := trainingCreateNextSample()
if err != nil {
errs = append(errs, err.Error())
continue
}
score := trainingPredictionUncertaintyScore(sample.Prediction)
if score > bestScore {
if best != nil {
trainingDeleteSampleFiles(root, best.SampleID)
}
best = sample
bestScore = score
} else {
trainingDeleteSampleFiles(root, sample.SampleID)
}
}
if best == nil {
if len(errs) > 0 {
return nil, errors.New(strings.Join(errs, "; "))
}
return nil, errors.New("keine unsicheren Trainingskandidaten gefunden")
}
publishAnalysisStep(
startedAtMs,
attempts+1,
attempts+1,
best.SourceFile,
fmt.Sprintf("Unsicherer Kandidat gewählt · Score %.0f%%", bestScore*100),
)
return best, nil
}
func trainingDeleteSampleFiles(root string, sampleID string) {
sampleID = strings.TrimSpace(sampleID)
if sampleID == "" || strings.Contains(sampleID, "/") || strings.Contains(sampleID, "\\") {
return
}
_ = os.Remove(filepath.Join(root, "samples", sampleID+".json"))
_ = os.Remove(filepath.Join(root, "frames", sampleID+".jpg"))
}
func trainingPredictionUncertaintyScore(pred TrainingPrediction) float64 {
if !pred.ModelAvailable {
return 0.10
}
scores := []float64{}
addScore := func(score float64) {
if math.IsNaN(score) || math.IsInf(score, 0) {
return
}
if score <= 0 {
return
}
scores = append(scores, clamp01(score))
}
if strings.TrimSpace(pred.SexPosition) != "" &&
strings.TrimSpace(pred.SexPosition) != "unknown" {
addScore(pred.SexPositionScore)
}
for _, box := range pred.Boxes {
addScore(box.Score)
}
if len(pred.Boxes) == 0 {
for _, item := range pred.BodyPartsPresent {
addScore(item.Score)
}
for _, item := range pred.ObjectsPresent {
addScore(item.Score)
}
for _, item := range pred.ClothingPresent {
addScore(item.Score)
}
}
if len(scores) == 0 {
// Modell ist verfügbar, erkennt aber nichts.
// Das kann ein nützliches Hard-Negative oder ein False-Negative sein.
return 0.35
}
sum := 0.0
for _, score := range scores {
// Höchste Unsicherheit ungefähr im mittleren Bereich.
// 0.55 ist absichtlich etwas niedriger als 0.75,
// damit Low/Mid-Confidence-Fälle bevorzugt werden.
distance := math.Abs(score - 0.55)
uncertainty := 1.0 - distance/0.55
sum += clamp01(uncertainty)
}
avg := sum / float64(len(scores))
// Viele Boxen mit mittlerer Confidence sind besonders wertvoll.
boxBonus := math.Min(0.12, float64(len(pred.Boxes))*0.025)
// Sehr niedrige Confidence soll ebenfalls auftauchen,
// aber nicht alle Ergebnisse dominieren.
lowConfidenceBonus := 0.0
for _, score := range scores {
if score >= 0.25 && score <= 0.75 {
lowConfidenceBonus = 0.08
break
}
}
return clamp01(avg + boxBonus + lowConfidenceBonus)
}
func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, error) { func trainingCreateNextSampleWithProgress(startedAtMs int64) (*TrainingSample, error) {
publishAnalysisStep(startedAtMs, 1, 4, "", "Video wird ausgewählt…") publishAnalysisStep(startedAtMs, 1, 4, "", "Video wird ausgewählt…")

View File

@ -184,6 +184,8 @@ type TrainingNotice = {
progress?: number progress?: number
} }
type TrainingSampleMode = 'random' | 'uncertain'
function trainingNoticeClass(kind: TrainingNoticeKind) { function trainingNoticeClass(kind: TrainingNoticeKind) {
switch (kind) { switch (kind) {
case 'success': case 'success':
@ -2042,6 +2044,7 @@ export default function TrainingTab(props: {
const [boxLabel, setBoxLabel] = useState('') const [boxLabel, setBoxLabel] = useState('')
const [activeBoxIndex, setActiveBoxIndex] = useState<number | null>(null) const [activeBoxIndex, setActiveBoxIndex] = useState<number | null>(null)
const [imageReloadKey, setImageReloadKey] = useState(0) const [imageReloadKey, setImageReloadKey] = useState(0)
const [trainingSampleMode, setTrainingSampleMode] = useState<TrainingSampleMode>('random')
const [expandedCorrectionSections, setExpandedCorrectionSections] = useState({ const [expandedCorrectionSections, setExpandedCorrectionSections] = useState({
sexPosition: false, sexPosition: false,
people: false, people: false,
@ -2293,12 +2296,17 @@ export default function TrainingTab(props: {
forceNew?: boolean forceNew?: boolean
refreshPrediction?: boolean refreshPrediction?: boolean
preserveNotice?: boolean preserveNotice?: boolean
mode?: TrainingSampleMode
}) => { }) => {
const mode = opts?.mode ?? trainingSampleMode
const uncertainMode = mode === 'uncertain' && !opts?.refreshPrediction
setLoading(true) setLoading(true)
setAnalysisProgress(8) setAnalysisProgress(8)
setAnalysisStep( setAnalysisStep(
opts?.refreshPrediction opts?.refreshPrediction
? 'Aktuelles Bild wird neu analysiert…' ? 'Aktuelles Bild wird neu analysiert…'
: uncertainMode
? 'Unsichere Prediction wird gesucht…'
: opts?.forceNew : opts?.forceNew
? 'Neues Trainingsbild wird gesucht…' ? 'Neues Trainingsbild wird gesucht…'
: 'Trainingsbild wird geladen…' : 'Trainingsbild wird geladen…'
@ -2314,11 +2322,16 @@ export default function TrainingTab(props: {
if (opts?.forceNew) params.set('force', '1') if (opts?.forceNew) params.set('force', '1')
if (opts?.refreshPrediction) params.set('refresh', '1') if (opts?.refreshPrediction) params.set('refresh', '1')
if (uncertainMode) params.set('mode', 'uncertain')
const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}` const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}`
setAnalysisProgress(25) setAnalysisProgress(25)
setAnalysisStep('Bild wird vorbereitet…') setAnalysisStep(
uncertainMode
? 'Mehrere Kandidaten werden bewertet…'
: 'Bild wird vorbereitet…'
)
const res = await fetch(url, { cache: 'no-store' }) const res = await fetch(url, { cache: 'no-store' })
const data = await res.json().catch(() => null) const data = await res.json().catch(() => null)
@ -2371,7 +2384,7 @@ export default function TrainingTab(props: {
setAnalysisStep('') setAnalysisStep('')
}, 500) }, 500)
} }
}, []) }, [trainingSampleMode])
const reloadCurrentImage = useCallback(async () => { const reloadCurrentImage = useCallback(async () => {
setDrawingBox(null) setDrawingBox(null)
@ -3431,6 +3444,64 @@ export default function TrainingTab(props: {
</button> </button>
</div> </div>
<div className="mt-3 rounded-xl bg-gray-50 p-2 ring-1 ring-black/5 dark:bg-white/[0.04] dark:ring-white/10">
<button
type="button"
disabled={uiLocked}
onClick={() => {
const nextMode: TrainingSampleMode =
trainingSampleMode === 'uncertain' ? 'random' : 'uncertain'
setTrainingSampleMode(nextMode)
if (!uiLocked) {
void loadNext({
forceNew: true,
mode: nextMode,
})
}
}}
className={[
'flex w-full items-center justify-between gap-3 rounded-lg px-2 py-1.5 text-left transition',
'focus:outline-none focus:ring-2 focus:ring-indigo-500/40',
uiLocked
? 'cursor-not-allowed opacity-50'
: 'hover:bg-white dark:hover:bg-white/10',
].join(' ')}
title="Wenn aktiv, werden bevorzugt Frames mit niedriger oder mittlerer Modell-Confidence geladen."
aria-pressed={trainingSampleMode === 'uncertain'}
>
<span className="min-w-0">
<span className="block text-[11px] font-bold text-gray-900 dark:text-white">
Unsichere zuerst
</span>
<span className="mt-0.5 block text-[10px] leading-snug text-gray-500 dark:text-gray-400">
Lädt bevorzugt Grenzfälle, die dem Modell am meisten helfen.
</span>
</span>
<span
className={[
'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition',
trainingSampleMode === 'uncertain'
? 'bg-indigo-600'
: 'bg-gray-300 dark:bg-white/20',
].join(' ')}
aria-hidden="true"
>
<span
className={[
'inline-block h-4 w-4 rounded-full bg-white shadow transition',
trainingSampleMode === 'uncertain'
? 'translate-x-4'
: 'translate-x-0.5',
].join(' ')}
/>
</span>
</button>
</div>
{trainingRunning || feedbackCount < requiredCount ? ( {trainingRunning || feedbackCount < requiredCount ? (
<div className="mt-3"> <div className="mt-3">
<div className="mb-1 flex items-center justify-between text-[10px] font-medium text-gray-500 dark:text-gray-400"> <div className="mb-1 flex items-center justify-between text-[10px] font-medium text-gray-500 dark:text-gray-400">
@ -4394,7 +4465,11 @@ export default function TrainingTab(props: {
disabled={uiLocked || frameBusy || !sample} disabled={uiLocked || frameBusy || !sample}
onClick={() => void loadNext({ forceNew: true })} onClick={() => void loadNext({ forceNew: true })}
className="w-full justify-center px-2 text-xs sm:text-sm" className="w-full justify-center px-2 text-xs sm:text-sm"
title="Dieses Bild nicht bewerten und ein anderes laden." title={
trainingSampleMode === 'uncertain'
? 'Dieses Bild nicht bewerten und eine andere unsichere Prediction laden.'
: 'Dieses Bild nicht bewerten und ein anderes laden.'
}
> >
<span className="sm:hidden">Skip</span> <span className="sm:hidden">Skip</span>
<span className="hidden sm:inline">Überspringen</span> <span className="hidden sm:inline">Überspringen</span>