3 — Modelo estatístico adaptativo: média, variância e decisão probabilística (online, leve e interpretável)
Nesta seção vamos construir o coração decisório do novo artigo: um modelo estatístico adaptativo, atualizado online, que decide se o vetor de features extraído pelo Goertzel se comporta como um assobio humano.
A proposta é deliberadamente simples, robusta e adequada a MCU:
- Sem redes neurais
- Sem matrizes grandes
- Sem treinamento pesado offline
- Totalmente determinística em tempo real
3.1 Princípio do modelo: aprender o “normal” e medir desvio
A ideia central é a seguinte:
Um assobio humano produz features que oscilam em torno de um padrão médio, com variabilidade limitada.
Logo, podemos modelar cada feature \(f_i\) como uma variável aleatória aproximadamente gaussiana, caracterizada por:
- Média \(\mu_i\)
- Variância \(\sigma_i^2\)
Quando um novo vetor chega:
- Se ele estiver próximo do padrão aprendido, aceitamos como assobio
- Se estiver longe demais, rejeitamos
Esse tipo de decisão é extremamente comum em:
- Detecção de anomalias
- Sensores industriais
- Sistemas embarcados adaptativos
3.2 Modelo estatístico por feature (independente)
Para manter o custo baixo, assumimos independência entre as features.
Isso evita cálculo de matriz de covariância (Mahalanobis completa).
Para cada feature \(f_i\), mantemos:
- média \(\mu_i\)
- variância \(\sigma_i^2\)
Estrutura em C:
typedef struct {
float mean;
float var;
} stat_feat_t;
E o modelo completo:
typedef struct {
stat_feat_t f1;
stat_feat_t f2;
stat_feat_t f3;
stat_feat_t f4;
} stat_model_t;
3.3 Atualização online da média (EMA)
A média é atualizada com uma média móvel exponencial:
\[
\mu[n] = \mu[n-1] + \alpha (x[n] – \mu[n-1])
\]
Onde:
- \(\alpha\) pequeno → aprendizado lento (estável)
- \(\alpha\) maior → aprendizado rápido (adaptativo)
Implementação genérica:
static inline void stat_update_mean(stat_feat_t *s, float x, float alpha)
{
s->mean += alpha * (x - s->mean);
}
3.4 Atualização online da variância (forma estável)
A variância pode ser atualizada de forma incremental como:
\[
\sigma^2[n] = (1-\alpha)\sigma^2[n-1] + \alpha (x[n]-\mu[n])^2
\]
Implementação:
static inline void stat_update_var(stat_feat_t *s, float x, float alpha)
{
float d = x - s->mean;
s->var = (1.0f - alpha) * s->var + alpha * (d * d);
}
Essa forma é estável numericamente e suficiente para aprendizado embarcado.
3.5 Inicialização segura do modelo
No boot, não temos dados. Precisamos:
- Evitar variância zero
- Evitar divisões instáveis
Inicialização recomendada:
static inline void stat_feat_init(stat_feat_t *s, float mean0, float var0)
{
s->mean = mean0;
s->var = var0;
}
static void stat_model_init(stat_model_t *m)
{
stat_feat_init(&m->f1, 0.5f, 0.1f);
stat_feat_init(&m->f2, 0.1f, 0.05f);
stat_feat_init(&m->f3, 0.05f, 0.05f);
stat_feat_init(&m->f4, 0.0f, 50.0f); // delta f0 em Hz
}
Esses valores não precisam ser perfeitos — o modelo aprende com o tempo.
3.6 Medida de desvio: Z-score por feature
Para decidir se uma feature é “normal”, usamos o Z-score:
\[
z_i = \frac{x_i – \mu_i}{\sqrt{\sigma_i^2 + \epsilon}}
\]
O valor absoluto indica quão longe estamos do padrão.
Implementação:
static inline float stat_zscore(const stat_feat_t *s, float x)
{
const float eps = 1e-6f;
float std = sqrtf(s->var + eps);
return (x - s->mean) / std;
}
3.7 Distância estatística simplificada (score global)
Agora combinamos os Z-scores das features:
\[
D = \sum_i z_i^2
\]
Isso é uma distância estatística diagonal, equivalente a uma Mahalanobis simplificada.
Implementação:
static float compute_stat_distance(const stat_model_t *m,
const feature_vec_t *v)
{
float z1 = stat_zscore(&m->f1, v->f1);
float z2 = stat_zscore(&m->f2, v->f2);
float z3 = stat_zscore(&m->f3, v->f3);
float z4 = stat_zscore(&m->f4, v->f4);
return (z1*z1) + (z2*z2) + (z3*z3) + (z4*z4);
}
Interpretação:
- D pequeno → padrão parecido com assobio aprendido
- D grande → padrão estranho (fala, ruído, batida)
3.8 Regra de decisão probabilística simples
Como (D) segue aproximadamente uma distribuição qui-quadrado com 4 graus de liberdade, usamos um limiar prático:
- (D < T_{ON}) → assobio
- (D > T_{OFF}) → não assobio
Com histerese:
#define D_ON 9.0f
#define D_OFF 12.0f
Implementação:
static bool stat_decision(float D, bool prev_state)
{
if (!prev_state) {
return (D < D_ON);
} else {
return (D < D_OFF);
}
}
3.9 Aprendizado controlado (não aprender ruído)
Regra de ouro:
👉 só atualize o modelo quando você acredita que o padrão é válido.
Estratégia:
- Se o estado atual é “assobio”
- Atualize média e variância
- Caso contrário, não aprenda
static void stat_model_update(stat_model_t *m,
const feature_vec_t *v,
float alpha)
{
stat_update_mean(&m->f1, v->f1, alpha);
stat_update_var (&m->f1, v->f1, alpha);
stat_update_mean(&m->f2, v->f2, alpha);
stat_update_var (&m->f2, v->f2, alpha);
stat_update_mean(&m->f3, v->f3, alpha);
stat_update_var (&m->f3, v->f3, alpha);
stat_update_mean(&m->f4, v->f4, alpha);
stat_update_var (&m->f4, v->f4, alpha);
}
3.10 O que conquistamos até aqui
Até este ponto, temos:
- Goertzel → features
- Features → modelo estatístico
- Modelo → distância probabilística
- Distância → decisão com histerese
- Aprendizado online, leve e estável
Tudo isso:
- Cabe facilmente no RP2040
- Não exige FPU pesada
- Não exige memória dinâmica
- É explicável e ajustável