import numpy as np, matplotlib.pyplot as plt
rng = np.random.default_rng(1)
X = np.vstack([rng.normal(m, 0.8, (150,2)) for m in ([0,0],[4,4],[5,0])]); N, K = len(X), 3
mu = X[rng.choice(N,K,replace=False)]; cov = np.array([np.eye(2)]*K); pi = np.ones(K)/K
def g(X,m,c): d=X-m; return np.exp(-0.5*np.sum(d@np.linalg.inv(c)*d,1))/np.sqrt((2*np.pi)**2*np.linalg.det(c))
lls=[]
for _ in range(25):
R = np.stack([pi[k]*g(X,mu[k],cov[k]) for k in range(K)],1) # E-step
lls.append(np.sum(np.log(R.sum(1)))); R /= R.sum(1,keepdims=True)
Nk = R.sum(0); mu = (R.T@X)/Nk[:,None] # M-step
for k in range(K): d=X-mu[k]; cov[k]=(R[:,k,None]*d).T@d/Nk[k]+1e-6*np.eye(2)
pi = Nk/N
fig,ax=plt.subplots(1,2,figsize=(6.8,2.8))
ax[0].scatter(X[:,0],X[:,1],c=R,s=8); ax[0].scatter(mu[:,0],mu[:,1],c="k",marker="x",s=80)
ax[0].set_title("data coloured by responsibility",fontsize=8); ax[0].set_xticks([]); ax[0].set_yticks([])
ax[1].plot(lls,"o-",color="#5d2c80"); ax[1].set_title("log-likelihood increases every step",fontsize=8)
ax[1].set_xlabel("EM iteration"); plt.tight_layout(); plt.show()