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" ||
|
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…")
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user