added uncertain training
This commit is contained in:
parent
2d5e522475
commit
cb7f32d24d
@ -443,6 +443,9 @@ func trainingNextHandler(w http.ResponseWriter, r *http.Request) {
|
||||
forceNew := r.URL.Query().Get("force") == "1" ||
|
||||
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()
|
||||
if err != nil {
|
||||
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" ||
|
||||
strings.EqualFold(r.URL.Query().Get("refresh"), "true")
|
||||
|
||||
if !forceNew {
|
||||
if !forceNew && !preferUncertain {
|
||||
var startedAtMs int64
|
||||
|
||||
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 {
|
||||
publishAnalysisError(startedAtMs, "", "Trainingsbild konnte nicht erstellt werden.", err)
|
||||
trainingWriteError(w, http.StatusInternalServerError, err.Error())
|
||||
@ -1824,6 +1840,153 @@ func trainingCreateNextSample() (*TrainingSample, error) {
|
||||
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) {
|
||||
publishAnalysisStep(startedAtMs, 1, 4, "", "Video wird ausgewählt…")
|
||||
|
||||
|
||||
@ -184,6 +184,8 @@ type TrainingNotice = {
|
||||
progress?: number
|
||||
}
|
||||
|
||||
type TrainingSampleMode = 'random' | 'uncertain'
|
||||
|
||||
function trainingNoticeClass(kind: TrainingNoticeKind) {
|
||||
switch (kind) {
|
||||
case 'success':
|
||||
@ -2042,6 +2044,7 @@ export default function TrainingTab(props: {
|
||||
const [boxLabel, setBoxLabel] = useState('')
|
||||
const [activeBoxIndex, setActiveBoxIndex] = useState<number | null>(null)
|
||||
const [imageReloadKey, setImageReloadKey] = useState(0)
|
||||
const [trainingSampleMode, setTrainingSampleMode] = useState<TrainingSampleMode>('random')
|
||||
const [expandedCorrectionSections, setExpandedCorrectionSections] = useState({
|
||||
sexPosition: false,
|
||||
people: false,
|
||||
@ -2293,15 +2296,20 @@ export default function TrainingTab(props: {
|
||||
forceNew?: boolean
|
||||
refreshPrediction?: boolean
|
||||
preserveNotice?: boolean
|
||||
mode?: TrainingSampleMode
|
||||
}) => {
|
||||
const mode = opts?.mode ?? trainingSampleMode
|
||||
const uncertainMode = mode === 'uncertain' && !opts?.refreshPrediction
|
||||
setLoading(true)
|
||||
setAnalysisProgress(8)
|
||||
setAnalysisStep(
|
||||
opts?.refreshPrediction
|
||||
? 'Aktuelles Bild wird neu analysiert…'
|
||||
: opts?.forceNew
|
||||
? 'Neues Trainingsbild wird gesucht…'
|
||||
: 'Trainingsbild wird geladen…'
|
||||
: uncertainMode
|
||||
? 'Unsichere Prediction wird gesucht…'
|
||||
: opts?.forceNew
|
||||
? 'Neues Trainingsbild wird gesucht…'
|
||||
: 'Trainingsbild wird geladen…'
|
||||
)
|
||||
|
||||
if (!opts?.preserveNotice) {
|
||||
@ -2314,11 +2322,16 @@ export default function TrainingTab(props: {
|
||||
|
||||
if (opts?.forceNew) params.set('force', '1')
|
||||
if (opts?.refreshPrediction) params.set('refresh', '1')
|
||||
if (uncertainMode) params.set('mode', 'uncertain')
|
||||
|
||||
const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
setAnalysisProgress(25)
|
||||
setAnalysisStep('Bild wird vorbereitet…')
|
||||
setAnalysisStep(
|
||||
uncertainMode
|
||||
? 'Mehrere Kandidaten werden bewertet…'
|
||||
: 'Bild wird vorbereitet…'
|
||||
)
|
||||
|
||||
const res = await fetch(url, { cache: 'no-store' })
|
||||
const data = await res.json().catch(() => null)
|
||||
@ -2371,7 +2384,7 @@ export default function TrainingTab(props: {
|
||||
setAnalysisStep('')
|
||||
}, 500)
|
||||
}
|
||||
}, [])
|
||||
}, [trainingSampleMode])
|
||||
|
||||
const reloadCurrentImage = useCallback(async () => {
|
||||
setDrawingBox(null)
|
||||
@ -3431,6 +3444,64 @@ export default function TrainingTab(props: {
|
||||
</button>
|
||||
</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 ? (
|
||||
<div className="mt-3">
|
||||
<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}
|
||||
onClick={() => void loadNext({ forceNew: true })}
|
||||
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="hidden sm:inline">Überspringen</span>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user