第15章:GitHub Pagesでのプロジェクト公開
15.1 GitHub Pagesの基本設定
GitHub Pagesとは
GitHub Pagesは、GitHubが提供する静的サイトホスティングサービスです。リポジトリから直接ウェブサイトを公開でき、以下の特徴があります:
- 無料: Public リポジトリは無料で利用可能
- HTTPS対応: 自動的にHTTPSが有効
- カスタムドメイン: 独自ドメインの設定可能
- Jekyll統合: 静的サイトジェネレーターが組み込み
基本的な有効化手順
リポジトリ設定での有効化
# リポジトリのSettings → Pages で設定
github_pages:
source:
branch: main # または gh-pages
path: /docs # または / (ルート)
custom_domain: ml-project.example.com
enforce_https: true
theme: minimal # Jekyllテーマ
公開方法の選択
- mainブランチの
/docs
フォルダproject-root/ ├── src/ # ソースコード ├── models/ # モデルファイル ├── docs/ # GitHub Pages用 │ ├── index.html │ ├── css/ │ └── js/ └── README.md
- gh-pagesブランチ
# gh-pagesブランチを作成 git checkout --orphan gh-pages git rm -rf . echo "GitHub Pages" > index.html git add index.html git commit -m "Initial GitHub Pages commit" git push origin gh-pages
- GitHub Actions経由
```yaml
.github/workflows/pages.yml
name: Deploy to GitHub Pages
on: push: branches: [main] workflow_dispatch:
permissions: contents: read pages: write id-token: write
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Build site
run: |
# ビルド処理
npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: ./dist
deploy: environment: name: github-pages url: $ runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2
### カスタムドメインの設定
#### DNSレコードの設定
Aレコード(Apex domain用)
185.199.108.153 185.199.109.153 185.199.110.153 185.199.111.153
CNAMEレコード(サブドメイン用)
docs.example.com -> username.github.io
#### CNAMEファイルの作成
```bash
# docs/CNAME または gh-pagesブランチのルートに配置
echo "ml-project.example.com" > CNAME
git add CNAME
git commit -m "Add custom domain"
git push
Jekyll設定
_config.yml
# _config.yml
title: AI/ML Project Documentation
description: >-
State-of-the-art machine learning models and experiments
baseurl: "/ml-project" # リポジトリ名
url: "https://username.github.io"
# ビルド設定
markdown: kramdown
theme: minima
# プラグイン
plugins:
- jekyll-feed
- jekyll-seo-tag
- jekyll-sitemap
# 除外ファイル
exclude:
- Gemfile
- Gemfile.lock
- node_modules/
- vendor/
- .git/
- src/
- models/
- data/
# コレクション(実験結果など)
collections:
experiments:
output: true
permalink: /experiments/:name/
models:
output: true
permalink: /models/:name/
15.2 ML/AIプロジェクトのデモページ作成
インタラクティブなモデルデモ
TensorFlow.jsを使用したブラウザ推論
<!-- index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Classification Demo</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<h1>画像分類デモ</h1>
<div class="upload-area" id="uploadArea">
<p>画像をドラッグ&ドロップまたはクリックして選択</p>
<input type="file" id="fileInput" accept="image/*" hidden>
</div>
<div id="preview" class="preview hidden">
<img id="previewImage" alt="Preview">
</div>
<button id="predictBtn" class="predict-btn hidden">予測実行</button>
<div id="results" class="results hidden">
<h2>予測結果</h2>
<div id="predictions"></div>
</div>
</div>
<script src="js/model-demo.js"></script>
</body>
</html>
モデルローディングと推論
// js/model-demo.js
class ModelDemo {
constructor() {
this.model = null;
this.labels = [];
this.initializeUI();
this.loadModel();
}
async loadModel() {
try {
// モデルの読み込み
this.model = await tf.loadLayersModel('./models/model.json');
// ラベルの読み込み
const response = await fetch('./models/labels.json');
this.labels = await response.json();
console.log('Model loaded successfully');
this.showStatus('モデル読み込み完了', 'success');
} catch (error) {
console.error('Model loading failed:', error);
this.showStatus('モデル読み込みエラー', 'error');
}
}
async predict(imageElement) {
if (!this.model) {
this.showStatus('モデルが読み込まれていません', 'error');
return;
}
// 画像の前処理
const tensor = tf.browser.fromPixels(imageElement)
.resizeNearestNeighbor([224, 224])
.toFloat()
.div(tf.scalar(255.0))
.expandDims();
// 推論実行
const predictions = await this.model.predict(tensor).data();
tensor.dispose();
// 結果を整形
const results = Array.from(predictions)
.map((prob, idx) => ({
label: this.labels[idx],
probability: prob
}))
.sort((a, b) => b.probability - a.probability)
.slice(0, 5);
this.displayResults(results);
}
displayResults(results) {
const predictionsDiv = document.getElementById('predictions');
predictionsDiv.innerHTML = '';
results.forEach(result => {
const resultDiv = document.createElement('div');
resultDiv.className = 'prediction-item';
resultDiv.innerHTML = `
<div class="label">${result.label}</div>
<div class="probability-bar">
<div class="probability-fill" style="width: ${result.probability * 100}%"></div>
</div>
<div class="probability-text">${(result.probability * 100).toFixed(2)}%</div>
`;
predictionsDiv.appendChild(resultDiv);
});
document.getElementById('results').classList.remove('hidden');
}
initializeUI() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const predictBtn = document.getElementById('predictBtn');
// ドラッグ&ドロップ
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
this.handleImage(file);
}
});
// クリックでファイル選択
uploadArea.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) this.handleImage(file);
});
// 予測ボタン
predictBtn.addEventListener('click', () => {
const img = document.getElementById('previewImage');
this.predict(img);
});
}
handleImage(file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.getElementById('previewImage');
img.src = e.target.result;
document.getElementById('preview').classList.remove('hidden');
document.getElementById('predictBtn').classList.remove('hidden');
};
reader.readAsDataURL(file);
}
showStatus(message, type) {
// ステータス表示の実装
console.log(`[${type}] ${message}`);
}
}
// 初期化
document.addEventListener('DOMContentLoaded', () => {
new ModelDemo();
});
モデル変換とデプロイ
PyTorchモデルのTensorFlow.js変換
# scripts/convert_model_to_tfjs.py
import torch
import tensorflow as tf
import tensorflowjs as tfjs
import json
class ModelConverter:
def __init__(self, pytorch_model_path, output_dir='./docs/models'):
self.pytorch_model_path = pytorch_model_path
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def convert_pytorch_to_tfjs(self):
"""PyTorchモデルをTensorFlow.js形式に変換"""
# PyTorchモデルを読み込み
model = torch.load(self.pytorch_model_path, map_location='cpu')
model.eval()
# ONNXに変換
dummy_input = torch.randn(1, 3, 224, 224)
onnx_path = self.output_dir / 'model.onnx'
torch.onnx.export(
model,
dummy_input,
onnx_path,
export_params=True,
opset_version=11,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
# ONNXからTensorFlowに変換
import onnx
from onnx_tf.backend import prepare
onnx_model = onnx.load(onnx_path)
tf_rep = prepare(onnx_model)
tf_rep.export_graph(str(self.output_dir / 'tf_model'))
# TensorFlow.jsに変換
tfjs.converters.convert_tf_saved_model(
str(self.output_dir / 'tf_model'),
str(self.output_dir)
)
print(f"Model converted and saved to {self.output_dir}")
def create_labels_json(self, class_names):
"""ラベルファイルを作成"""
labels_path = self.output_dir / 'labels.json'
with open(labels_path, 'w') as f:
json.dump(class_names, f, ensure_ascii=False, indent=2)
def optimize_for_web(self):
"""Web用に最適化"""
# 量子化
converter = tf.lite.TFLiteConverter.from_saved_model(
str(self.output_dir / 'tf_model')
)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
# TFLiteモデルを保存
tflite_path = self.output_dir / 'model_quantized.tflite'
with open(tflite_path, 'wb') as f:
f.write(tflite_model)
print(f"Quantized model saved to {tflite_path}")
15.3 ドキュメントサイトの構築
MkDocsを使用した技術ドキュメント
mkdocs.yml設定
# mkdocs.yml
site_name: ML Project Documentation
site_url: https://username.github.io/ml-project
repo_url: https://github.com/username/ml-project
repo_name: username/ml-project
theme:
name: material
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- search.suggest
- search.highlight
- content.code.copy
palette:
- scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/brightness-4
name: Switch to light mode
plugins:
- search
- mkdocstrings:
handlers:
python:
setup_commands:
- import sys
- sys.path.insert(0, "src")
- git-revision-date-localized
- minify:
minify_html: true
markdown_extensions:
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.arithmatex:
generic: true
- admonition
- toc:
permalink: true
nav:
- Home: index.md
- Getting Started:
- Installation: getting-started/installation.md
- Quick Start: getting-started/quickstart.md
- Configuration: getting-started/configuration.md
- Models:
- Overview: models/overview.md
- ResNet: models/resnet.md
- EfficientNet: models/efficientnet.md
- Vision Transformer: models/vit.md
- API Reference:
- Data Loading: api/data.md
- Models: api/models.md
- Training: api/training.md
- Evaluation: api/evaluation.md
- Experiments:
- Baseline: experiments/baseline.md
- Hyperparameter Search: experiments/hparam-search.md
- Ablation Studies: experiments/ablation.md
- Examples:
- Image Classification: examples/image-classification.md
- Transfer Learning: examples/transfer-learning.md
- Fine-tuning: examples/fine-tuning.md
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/username
- icon: fontawesome/brands/twitter
link: https://twitter.com/username
analytics:
provider: google
property: G-XXXXXXXXXX
extra_javascript:
- javascripts/mathjax.js
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
Jupyter Notebookの統合
NotebookをHTMLに変換
# scripts/notebook_to_docs.py
import nbconvert
from pathlib import Path
import shutil
class NotebookConverter:
def __init__(self, notebooks_dir='notebooks', output_dir='docs/notebooks'):
self.notebooks_dir = Path(notebooks_dir)
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
def convert_all_notebooks(self):
"""すべてのNotebookをHTMLに変換"""
html_exporter = nbconvert.HTMLExporter()
html_exporter.template_name = 'lab'
for notebook_path in self.notebooks_dir.glob('**/*.ipynb'):
# チェックポイントをスキップ
if '.ipynb_checkpoints' in str(notebook_path):
continue
# 相対パスを保持
relative_path = notebook_path.relative_to(self.notebooks_dir)
output_path = self.output_dir / relative_path.with_suffix('.html')
output_path.parent.mkdir(parents=True, exist_ok=True)
# 変換実行
(body, resources) = html_exporter.from_filename(str(notebook_path))
# カスタムCSSを追加
custom_css = """
<style>
.jp-OutputArea-output pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
}
.jp-RenderedHTMLCommon img {
max-width: 100%;
height: auto;
}
</style>
"""
body = body.replace('</head>', f'{custom_css}</head>')
# ファイルを保存
with open(output_path, 'w', encoding='utf-8') as f:
f.write(body)
print(f"Converted: {notebook_path} -> {output_path}")
# 画像などのリソースをコピー
if 'outputs' in resources:
for filename, data in resources['outputs'].items():
resource_path = output_path.parent / filename
with open(resource_path, 'wb') as f:
f.write(data)
def create_index(self):
"""Notebook一覧ページを作成"""
index_content = """# Jupyter Notebooks
このセクションでは、プロジェクトで使用されているJupyter Notebookを閲覧できます。
## Notebooks一覧
"""
for notebook_html in self.output_dir.glob('**/*.html'):
relative_path = notebook_html.relative_to(self.output_dir)
name = notebook_html.stem.replace('_', ' ').title()
index_content += f"- [{name}]({relative_path})\n"
index_path = self.output_dir / 'index.md'
with open(index_path, 'w', encoding='utf-8') as f:
f.write(index_content)
APIドキュメントの自動生成
Sphinxを使用したAPIドキュメント
# docs/conf.py
import os
import sys
sys.path.insert(0, os.path.abspath('../src'))
project = 'ML Project'
copyright = '2024, Your Name'
author = 'Your Name'
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'sphinx_rtd_theme',
'sphinx.ext.mathjax',
'myst_parser'
]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
# Napoleon設定
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = True
# MyST設定
myst_enable_extensions = [
"dollarmath",
"amsmath",
"deflist",
"html_image",
]
# 自動ドキュメント設定
autodoc_default_options = {
'members': True,
'member-order': 'bysource',
'special-members': '__init__',
'undoc-members': True,
'exclude-members': '__weakref__'
}
15.4 実験結果の可視化と公開
インタラクティブなダッシュボード
Plotlyを使用した可視化
<!-- experiments/dashboard.html -->
<!DOCTYPE html>
<html>
<head>
<title>Experiment Results Dashboard</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
.dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.metric-card {
background: #f5f5f5;
border-radius: 8px;
padding: 20px;
margin: 10px 0;
}
.plot-container {
margin: 20px 0;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="dashboard">
<h1>ML Experiment Results</h1>
<div class="metric-card">
<h2>Latest Metrics</h2>
<div id="metrics-summary"></div>
</div>
<div class="plot-container">
<h3>Training Progress</h3>
<div id="training-plot"></div>
</div>
<div class="plot-container">
<h3>Model Comparison</h3>
<div id="comparison-plot"></div>
</div>
<div class="plot-container">
<h3>Hyperparameter Analysis</h3>
<div id="hparam-plot"></div>
</div>
</div>
<script src="js/dashboard.js"></script>
</body>
</html>
ダッシュボードのJavaScript
// js/dashboard.js
class ExperimentDashboard {
constructor() {
this.loadExperimentData();
}
async loadExperimentData() {
try {
const response = await fetch('./data/experiments.json');
this.data = await response.json();
this.renderDashboard();
} catch (error) {
console.error('Failed to load experiment data:', error);
}
}
renderDashboard() {
this.renderMetricsSummary();
this.renderTrainingPlot();
this.renderComparisonPlot();
this.renderHyperparameterPlot();
}
renderMetricsSummary() {
const latest = this.data.experiments[this.data.experiments.length - 1];
const summaryHtml = `
<div class="metrics-grid">
<div class="metric">
<h4>Best Accuracy</h4>
<p class="value">${(latest.best_accuracy * 100).toFixed(2)}%</p>
</div>
<div class="metric">
<h4>Training Time</h4>
<p class="value">${latest.training_time} hours</p>
</div>
<div class="metric">
<h4>Model Size</h4>
<p class="value">${latest.model_size} MB</p>
</div>
<div class="metric">
<h4>Inference Time</h4>
<p class="value">${latest.inference_time} ms</p>
</div>
</div>
`;
document.getElementById('metrics-summary').innerHTML = summaryHtml;
}
renderTrainingPlot() {
const traces = this.data.experiments.map(exp => ({
x: exp.epochs,
y: exp.train_loss,
name: `${exp.name} (train)`,
type: 'scatter',
mode: 'lines'
}));
// Validation lossも追加
this.data.experiments.forEach(exp => {
traces.push({
x: exp.epochs,
y: exp.val_loss,
name: `${exp.name} (val)`,
type: 'scatter',
mode: 'lines',
line: { dash: 'dot' }
});
});
const layout = {
title: 'Training and Validation Loss',
xaxis: { title: 'Epoch' },
yaxis: { title: 'Loss' },
hovermode: 'x unified'
};
Plotly.newPlot('training-plot', traces, layout);
}
renderComparisonPlot() {
const models = this.data.experiments.map(exp => exp.name);
const metrics = ['accuracy', 'precision', 'recall', 'f1_score'];
const traces = metrics.map(metric => ({
x: models,
y: this.data.experiments.map(exp => exp.metrics[metric]),
name: metric.charAt(0).toUpperCase() + metric.slice(1),
type: 'bar'
}));
const layout = {
title: 'Model Performance Comparison',
xaxis: { title: 'Model' },
yaxis: { title: 'Score' },
barmode: 'group'
};
Plotly.newPlot('comparison-plot', traces, layout);
}
renderHyperparameterPlot() {
// 3D散布図でハイパーパラメータと性能の関係を表示
const trace = {
x: this.data.experiments.map(exp => exp.hyperparameters.learning_rate),
y: this.data.experiments.map(exp => exp.hyperparameters.batch_size),
z: this.data.experiments.map(exp => exp.metrics.accuracy),
mode: 'markers',
marker: {
size: 12,
color: this.data.experiments.map(exp => exp.metrics.accuracy),
colorscale: 'Viridis',
showscale: true
},
text: this.data.experiments.map(exp => exp.name),
type: 'scatter3d'
};
const layout = {
title: 'Hyperparameter Space Exploration',
scene: {
xaxis: { title: 'Learning Rate', type: 'log' },
yaxis: { title: 'Batch Size' },
zaxis: { title: 'Accuracy' }
}
};
Plotly.newPlot('hparam-plot', [trace], layout);
}
}
// 初期化
document.addEventListener('DOMContentLoaded', () => {
new ExperimentDashboard();
});
実験結果の自動更新
GitHub Actionsによる結果収集
# .github/workflows/update-results.yml
name: Update Experiment Results
on:
workflow_run:
workflows: ["Model Training"]
types:
- completed
jobs:
update-results:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: gh-pages
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: experiment-results
path: ./temp
- name: Process results
run: |
python scripts/process_results.py \
--input ./temp/results.json \
--output ./data/experiments.json
- name: Generate plots
run: |
python scripts/generate_plots.py \
--data ./data/experiments.json \
--output ./images/
- name: Commit and push
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add data/experiments.json images/
git commit -m "Update experiment results - $(date +'%Y-%m-%d %H:%M:%S')"
git push
15.5 自動デプロイとメンテナンス
CI/CDパイプライン
完全な自動デプロイワークフロー
# .github/workflows/deploy-docs.yml
name: Deploy Documentation
on:
push:
branches: [main]
paths:
- 'docs/**'
- 'src/**'
- 'notebooks/**'
- 'mkdocs.yml'
schedule:
- cron: '0 2 * * 1' # 毎週月曜日に再ビルド
jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history for git-revision-date
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements-docs.txt
pip install -e . # プロジェクトをインストール
- name: Convert notebooks
run: |
python scripts/notebook_to_docs.py
- name: Generate API documentation
run: |
sphinx-apidoc -o docs/api src/
- name: Build MkDocs
run: |
mkdocs build --clean
- name: Generate experiment results
run: |
python scripts/collect_experiments.py
cp -r experiments/* site/experiments/
- name: Optimize assets
run: |
# 画像の最適化
find site -name "*.png" -exec optipng {} \;
find site -name "*.jpg" -exec jpegoptim {} \;
# HTMLの圧縮
find site -name "*.html" -exec html-minifier \
--collapse-whitespace \
--remove-comments \
--minify-css true \
--minify-js true \
-o {} {} \;
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: $
publish_dir: ./site
cname: ml-docs.example.com
パフォーマンス最適化
Service Workerでのキャッシュ
// sw.js - Service Worker
const CACHE_NAME = 'ml-docs-v1';
const urlsToCache = [
'/',
'/css/style.css',
'/js/model-demo.js',
'/models/model.json',
'/models/labels.json'
];
// インストール時にキャッシュ
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// フェッチ時にキャッシュを確認
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// キャッシュがあればそれを返す
if (response) {
return response;
}
// なければネットワークから取得
return fetch(event.request).then(response => {
// レスポンスが有効でない場合はそのまま返す
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// レスポンスをキャッシュに保存
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
モニタリングとアナリティクス
カスタムアナリティクス実装
// analytics.js
class MLDocsAnalytics {
constructor() {
this.sessionId = this.generateSessionId();
this.events = [];
this.startTime = Date.now();
// ページビューを記録
this.trackPageView();
// イベントリスナーを設定
this.setupEventListeners();
}
generateSessionId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
trackPageView() {
this.track('page_view', {
url: window.location.href,
referrer: document.referrer,
screen: `${window.screen.width}x${window.screen.height}`
});
}
setupEventListeners() {
// モデルデモの使用を追跡
document.addEventListener('model_prediction', (e) => {
this.track('model_demo_used', {
model: e.detail.model,
prediction_time: e.detail.time
});
});
// ドキュメントのスクロール深度
let maxScroll = 0;
window.addEventListener('scroll', () => {
const scrollPercentage = (window.scrollY / document.body.scrollHeight) * 100;
maxScroll = Math.max(maxScroll, scrollPercentage);
});
// ページ離脱時に送信
window.addEventListener('beforeunload', () => {
this.track('page_leave', {
time_on_page: Date.now() - this.startTime,
max_scroll_depth: maxScroll
});
this.sendEvents();
});
}
track(eventName, properties = {}) {
this.events.push({
event: eventName,
properties: {
...properties,
session_id: this.sessionId,
timestamp: new Date().toISOString()
}
});
}
sendEvents() {
if (this.events.length === 0) return;
// Beacon APIを使用して確実に送信
const data = JSON.stringify({
events: this.events,
site: 'ml-docs'
});
navigator.sendBeacon('/api/analytics', data);
this.events = [];
}
}
// 初期化(プライバシーに配慮)
if (!window.location.hostname.includes('localhost')) {
new MLDocsAnalytics();
}
メンテナンススクリプト
リンクチェッカー
# scripts/check_links.py
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import concurrent.futures
class LinkChecker:
def __init__(self, base_url):
self.base_url = base_url
self.checked_urls = set()
self.broken_links = []
def check_site(self):
"""サイト全体のリンクをチェック"""
self.check_page(self.base_url)
if self.broken_links:
print(f"\nFound {len(self.broken_links)} broken links:")
for link in self.broken_links:
print(f" - {link['url']} (found on {link['source']})")
else:
print("No broken links found!")
def check_page(self, url):
"""ページ内のリンクをチェック"""
if url in self.checked_urls:
return
self.checked_urls.add(url)
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
except Exception as e:
self.broken_links.append({
'url': url,
'source': 'direct check',
'error': str(e)
})
return
# HTMLを解析
soup = BeautifulSoup(response.text, 'html.parser')
links = soup.find_all(['a', 'link', 'script', 'img'])
# 並列でリンクをチェック
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = []
for tag in links:
href = tag.get('href') or tag.get('src')
if not href:
continue
absolute_url = urljoin(url, href)
# 同じドメインの場合は再帰的にチェック
if urlparse(absolute_url).netloc == urlparse(self.base_url).netloc:
if absolute_url not in self.checked_urls:
futures.append(
executor.submit(self.check_page, absolute_url)
)
else:
# 外部リンクは存在確認のみ
futures.append(
executor.submit(self.check_external_link, absolute_url, url)
)
concurrent.futures.wait(futures)
def check_external_link(self, url, source):
"""外部リンクの存在確認"""
try:
response = requests.head(url, timeout=10, allow_redirects=True)
response.raise_for_status()
except Exception as e:
self.broken_links.append({
'url': url,
'source': source,
'error': str(e)
})
# 実行
if __name__ == '__main__':
checker = LinkChecker('https://username.github.io/ml-project')
checker.check_site()
まとめ
本章では、GitHub Pagesを活用したプロジェクト公開について学習しました:
- GitHub Pagesの基本設定とカスタムドメイン
- TensorFlow.jsを使用したインタラクティブなモデルデモ
- MkDocsやSphinxによる技術ドキュメントの構築
- 実験結果の可視化とダッシュボード作成
- CI/CDによる自動デプロイとメンテナンス
次章では、外部協力者との連携について学習します。
確認事項
- GitHub Pagesを有効化できる
- 静的サイトジェネレーターを選択・設定できる
- モデルをブラウザで実行できる形式に変換できる
- ドキュメントの自動生成パイプラインを構築できる
- パフォーマンスとアクセシビリティに配慮したサイトを作成できる