简述
使用html实现HMM模型的动画演示,直接存储为txt文件更改后辍名为html即可运行,该效果只展示了状态转移。
使用参数如下
状态:[“下雨”,“晴天”]
观察结果:[“步行”、“购物”、“清洁”]
转移概率:
“下雨”:{“下雨”:0.7,“晴天”:0.3}
“晴天”:{“下雨”:0.4,“晴天”:0.6}
发射概率:
“雨天”:{“步行”:0.1,“商店”:0.4,“清洁”:0.5}
“晴天”:{“步行”:0.6,“购物”:0.3,“清洁”:0.1}
可视化解释
红点所在为当前状态,箭头为转移方向,灰色为前一个状态。
实现效果
Rainy转移状态Rainy:
Rainy状态转移Sunny:
完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMM算法可视化</title>
<style>
.container {
width: 800px;
margin: 20px auto;
text-align: center;
}
svg {
border: 1px solid #ccc;
}
button {
padding: 10px 20px;
margin-top: 20px;
cursor: pointer;
}
.state-node {
cursor: pointer;
transition: fill 0.3s ease;
}
.status {
font-size: 20px;
margin-top: 20px;
font-weight: bold;
}
.arrow {
stroke: black;
stroke-width: 2;
}
</style>
</head>
<body>
<div class="container">
<h1>HMM算法可视化</h1>
<svg id="hmmCanvas" width="800" height="400"></svg>
<button onclick="startAnimation()">开始动画</button>
<button onclick="stopAnimation()">停止动画</button>
<!-- 显示当前状态 -->
<div class="status" id="currentStatus">当前状态: Rainy</div>
</div>
<script>
const svg = document.getElementById('hmmCanvas');
const statusDiv = document.getElementById('currentStatus'); // 获取状态显示区域
let isAnimating = false;
let currentState = 'Rainy'; // 初始状态
const states = ['Rainy', 'Sunny'];
let animationTimeouts = []; // 存储所有setTimeout的ID,以便清除
// HMM参数设置
const hmm = {
states: ['Rainy', 'Sunny'],
observations: ['Walk', 'Shop', 'Clean'],
transitionProb: {
'Rainy': {'Rainy': 0.7, 'Sunny': 0.3},
'Sunny': {'Rainy': 0.4, 'Sunny': 0.6}
},
emissionProb: {
'Rainy': {'Walk': 0.1, 'Shop': 0.4, 'Clean': 0.5},
'Sunny': {'Walk': 0.6, 'Shop': 0.3, 'Clean': 0.1}
}
};
// 初始化SVG绘图
function initializeSVG() {
svg.innerHTML = '';
hmm.states.forEach((state, i) => {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('transform', `translate(${200 + i * 300}, 200)`);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('r', '40');
circle.setAttribute('fill', state === 'Rainy' ? '#42a5f5' : '#ffd600');
circle.classList.add('state-node');
circle.setAttribute('data-state', state);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dy', '5');
text.textContent = state;
g.appendChild(circle);
g.appendChild(text);
svg.appendChild(g);
});
// 创建红色圆圈表示当前状态
createCurrentStateIndicator();
}
// 创建一个红色圆圈表示当前状态
function createCurrentStateIndicator() {
const indicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
indicator.setAttribute('r', '10');
indicator.setAttribute('fill', 'red');
indicator.setAttribute('class', 'current-state-indicator');
svg.appendChild(indicator);
updateCurrentStateIndicator();
}
// 更新当前状态的红色圆圈位置
function updateCurrentStateIndicator() {
const stateIndex = hmm.states.indexOf(currentState);
const x = 200 + stateIndex * 300; // 计算红色圆圈X位置
const y = 240; // 红色圆圈放在状态球下方
const indicator = svg.querySelector('.current-state-indicator');
if (indicator) {
indicator.setAttribute('cx', x);
indicator.setAttribute('cy', y);
}
}
// 计算下一个状态
function getNextState(current) {
const rand = Math.random();
const prob = hmm.transitionProb[current];
return rand < prob['Rainy'] ? 'Rainy' : 'Sunny';
}
function calculateArrowPositions(fromState, toState) {
const fromIndex = hmm.states.indexOf(fromState);
const toIndex = hmm.states.indexOf(toState);
// 判断箭头方向,确保箭头的长度一致
if (fromIndex > toIndex) {
// 如果箭头从右向左(从 Sunny 到 Rainy)
const fromX = 200 + fromIndex * 300 - 40; // 半径偏移量,圆的右侧
const fromY = 200;
const toX = 200 + toIndex * 300 + 40; // 半径偏移量,圆的左侧
const toY = 200;
return {
fromX: fromX,
fromY: fromY,
toX: toX,
toY: toY,
};
} else {
// 如果箭头从左向右(从 Rainy 到 Sunny)
const fromX = 200 + fromIndex * 300 + 40; // 半径偏移量,圆的右侧
const fromY = 200;
const toX = 200 + toIndex * 300 - 40; // 半径偏移量,圆的左侧
const toY = 200;
return {
fromX: fromX,
fromY: fromY,
toX: toX,
toY: toY,
};
}
}
// 状态转移动画
function animateTransition(fromState, toState) {
const fromNode = svg.querySelector(`circle[data-state='${fromState}']`);
const toNode = svg.querySelector(`circle[data-state='${toState}']`);
// 在状态转换之间添加箭头
const { fromX, fromY, toX, toY } = calculateArrowPositions(fromState, toState);
// 画箭头
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
arrow.setAttribute('x1', fromX);
arrow.setAttribute('y1', fromY);
arrow.setAttribute('x2', toX);
arrow.setAttribute('y2', toY);
arrow.setAttribute('class', 'arrow');
svg.appendChild(arrow);
// 添加箭头尾部(三角形)指示方向
const angle = Math.atan2(toY - fromY, toX - fromX);
const arrowHeadSize = 10;
const arrowHeadX = toX - arrowHeadSize * Math.cos(angle - Math.PI / 6);
const arrowHeadY = toY - arrowHeadSize * Math.sin(angle - Math.PI / 6);
const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
arrowHead.setAttribute('points', `${toX},${toY} ${arrowHeadX},${arrowHeadY} ${toX - arrowHeadSize * Math.cos(angle + Math.PI / 6)},${toY - arrowHeadSize * Math.sin(angle + Math.PI / 6)}`);
arrowHead.setAttribute('fill', 'black');
svg.appendChild(arrowHead);
if (fromNode) fromNode.setAttribute('fill', 'gray'); // 旧状态变灰
setTimeout(() => {
if (fromNode) fromNode.setAttribute('fill', fromState === 'Rainy' ? '#42a5f5' : '#ffd600');
if (toNode) toNode.setAttribute('fill', toState === 'Rainy' ? '#42a5f5' : '#ffd600');
}, 3500);
// 移除箭头和箭头尾部
setTimeout(() => {
svg.removeChild(arrow);
svg.removeChild(arrowHead);
}, 3500);
}
// 生成观测
function displayObservation(state) {
const obsProb = hmm.emissionProb[state];
const rand = Math.random();
let chosenObs = 'Walk'; // 默认值
let cumulative = 0;
for (let obs in obsProb) {
cumulative += obsProb[obs];
if (rand < cumulative) {
chosenObs = obs;
break;
}
}
// 获取对应状态节点的位置
const stateNode = svg.querySelector(`circle[data-state='${state}']`);
if (!stateNode) return;
const obsText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
obsText.setAttribute('x', 350);
obsText.setAttribute('y', 100);
obsText.setAttribute('fill', '#2e7d32');
obsText.textContent = chosenObs;
obsText.setAttribute('opacity', 0);
svg.appendChild(obsText);
// 淡入效果
obsText.style.transition = 'opacity 0.5s ease-in';
setTimeout(() => obsText.setAttribute('opacity', 1), 100);
// 2秒后移除
setTimeout(() => svg.removeChild(obsText), 4000);
}
// 更新当前状态
function updateCurrentStatus(state) {
statusDiv.textContent = `当前状态: ${state}`; // 更新状态显示
currentState = state;
updateCurrentStateIndicator(); // 更新红色圆圈位置
}
// 启动动画
function startAnimation() {
if (isAnimating) return;
isAnimating = true;
function step() {
const nextState = getNextState(currentState);
animateTransition(currentState, nextState);
displayObservation(nextState);
updateCurrentStatus(nextState); // 更新当前状态显示
currentState = nextState;
if (isAnimating) {
const timeoutId = setTimeout(step, 4000); // 2秒后继续
animationTimeouts.push(timeoutId); // 保存timeout ID
}
}
step();
}
// 停止动画
function stopAnimation() {
isAnimating = false;
// 清除所有未执行的 setTimeout
animationTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
animationTimeouts = []; // 清空 timeout ID 列表
}
initializeSVG();
</script>
</body>
</html>