はじめに
日々のITニュース(RSS)を自動で収集・分類し、ダッシュボードで閲覧できるシステムを構築しました。
本記事では、最新のNVIDIA GeForce RTX 5060 Ti (Blackwellアーキテクチャ / sm_120) 環境においてPyTorchでファインチューニングを行い、PostgreSQLへ効率的にUpsert保存、さらにFastAPI + systemdでWebサービスとして常駐化させるまでの一連の手順とハマりどころの解決法をまとめます。
システム構成
- OS: Ubuntu Linux 24.04 LTS
-
GPU: NVIDIA GeForce RTX 5060 Ti (Compute Capability:
sm_120) - Python: 3.12 (venv)
-
Frameworks: PyTorch (Nightly cu128), Hugging Face Transformers (
cl-tohoku/bert-base-japanese), FastAPI, SQLAlchemy / psycopg2 -
DB: PostgreSQL 16 (
itnews_db)
1. RTX 5060 Ti (sm_120) 環境での PyTorch セットアップ
RTX 5000シリーズ(Blackwellアーキテクチャ)で既存のPyTorch(CUDA 12.1/12.4等)を動かすと、以下のようなカーネル未対応エラーが発生することがあります。
RuntimeError: CUDA error: no kernel image is available for execution on the device
これを回避するため、CUDA 12.8以降に対応した Nightlyビルドの PyTorch を導入します。
# 仮想環境内でのインストール
python -m pip install --pre torch torchvision torchaudio --index-url [https://download.pytorch.org/whl/nightly/cu128](https://download.pytorch.org/whl/nightly/cu128)
GPU認識の確認
import torch
print('CUDA Available:', torch.cuda.is_available())
print('Device Name:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'None')
CUDA Available: True,Device Name: NVIDIA GeForce RTX 5060 Tiが表示されれば準備完了です。
2. PostgreSQL への Upsert パイプライン (predict_rss.py)
推論したカテゴリと確信度(Confidence)をデータベースへ直接一括挿入・更新(Upsert)します。URL (link) を重複チェックキーとして ON CONFLICT 構文を使用します。
import os
import torch
import torch.nn.functional as F
import pandas as pd
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sqlalchemy import create_engine, text
from sqlalchemy.engine import URL
MODEL_DIR = "./saved_model"
INPUT_CSV = "./data/latest_rss_news.csv"
# DB接続設定
db_url = URL.create(
drivername="postgresql+psycopg2",
username="postgres",
password="your_password",
host="localhost",
port=5432,
database="itnews_db",
)
CATEGORY_MAP = {
0: "OpenSource/OSS", 1: "AI/Data", 2: "Enterprise/Cloud",
3: "Software/Dev", 4: "Hardware/PC", 5: "Mobile/Gadget",
6: "Security", 7: "Network/Telecom", 8: "Business/Tech"
}
def main():
df = pd.read_csv(INPUT_CSV)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_DIR).to(device)
model.eval()
titles = df["title"].fillna("").astype(str).tolist()
full_texts = df["full_text"].fillna("").astype(str).tolist()
texts = [f"{t} {ft}".strip() for t, ft in zip(titles, full_texts)]
# バッチ推論
batch_size = 32
predicted_cat_ids, predicted_cats, confidences = [], [], []
for i in range(0, len(texts), batch_size):
batch_texts = texts[i:i+batch_size]
inputs = tokenizer(batch_texts, max_length=256, padding=True, truncation=True, return_tensors="pt").to(device)
with torch.no_grad():
outputs = model(**inputs)
probs = F.softmax(outputs.logits, dim=-1)
max_probs, preds = torch.max(probs, dim=-1)
for pred_id, prob in zip(preds.cpu().numpy(), max_probs.cpu().numpy()):
predicted_cat_ids.append(int(pred_id))
predicted_cats.append(CATEGORY_MAP.get(int(pred_id), "Business/Tech"))
confidences.append(round(float(prob), 4))
df["category_id"] = predicted_cat_ids
df["predicted_category"] = predicted_cats
df["confidence"] = confidences
# DBへの保存 (Upsert)
engine = create_engine(db_url, connect_args={"client_encoding": "utf8"})
upsert_sql = text("""
INSERT INTO news (title, summary, full_text, link, published, published_date, source, category_id, category_name, confidence)
VALUES (:title, :summary, :full_text, :link, :published, :published_date, :source, :category_id, :category_name, :confidence)
ON CONFLICT (link) DO UPDATE SET
title = EXCLUDED.title,
category_id = EXCLUDED.category_id,
category_name = EXCLUDED.category_name,
confidence = EXCLUDED.confidence;
""")
with engine.begin() as conn:
for _, row in df.iterrows():
conn.execute(upsert_sql, {
"title": str(row.get("title", "")),
"summary": str(row.get("summary", "")),
"full_text": str(row.get("full_text", "")),
"link": str(row.get("link", "")),
"published": pd.to_datetime(row.get("published"), errors="coerce"),
"published_date": str(row.get("published_date", "")),
"source": str(row.get("source", "不明")),
"category_id": int(row.get("category_id", 8)),
"category_name": str(row.get("predicted_category", "Business/Tech")),
"confidence": float(row.get("confidence", 0.0))
})
if __name__ == "__main__":
main()
3. BERTモデルの再学習 (train.py)
Transformers v5系の仕様変更(evaluation_strategy $\rightarrow$ eval_strategy 等)に対応した学習スクリプトです。FP16(混合精度学習)を有効化し、RTX 5060 Ti の性能をフル活用します。
import torch
import pandas as pd
from sqlalchemy import create_engine
from sqlalchemy.engine import URL
from sklearn.model_selection import train_test_split
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
from datasets import Dataset
# DBから学習用データをロード
engine = create_engine(db_url, connect_args={"client_encoding": "utf8"})
df = pd.read_sql("SELECT title, summary, full_text, category_id FROM news WHERE category_id IS NOT NULL", engine)
df["text"] = df["title"] + " " + df["summary"].fillna("") + " " + df["full_text"].fillna("")
df["label"] = df["category_id"].astype(int)
train_df, val_df = train_test_split(df[["text", "label"]], test_size=0.2, random_state=42)
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
def tokenize_fn(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True, max_length=256)
train_ds = Dataset.from_pandas(train_df).map(tokenize_fn, batched=True)
val_ds = Dataset.from_pandas(val_df).map(tokenize_fn, batched=True)
model = AutoModelForSequenceClassification.from_pretrained("cl-tohoku/bert-base-japanese", num_labels=9)
use_gpu = torch.cuda.is_available()
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=16 if use_gpu else 8,
fp16=use_gpu, # GPU使用時のみ混合精度学習を有効化
eval_strategy="epoch", # transformers 5.x 準拠
save_strategy="epoch",
learning_rate=2e-5,
logging_strategy="steps",
load_best_model_at_end=True,
report_to="none"
)
trainer = Trainer(model=model, args=training_args, train_dataset=train_ds, eval_dataset=val_ds)
trainer.train()
model.save_pretrained("./saved_model")
tokenizer.save_pretrained("./saved_model")
4. systemd による Web サーバー常駐化
FastAPI (app.py) を Linux サービスとしてバックグラウンドで自動起動・常駐化させます。
サービスファイルの作成
/etc/systemd/system/itnews-web.service
[Unit]
Description=IT News FastAPI Web Server & Dashboard
After=network.target postgresql.service
[Service]
Type=simple
User=yyoshida
WorkingDirectory=/home/yyoshida/workspace/gputest
ExecStart=/home/yyoshida/workspace/gputest/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8000
Restart=always
[Install]
WantedBy=multi-user.target
サービスの有効化と起動
sudo systemctl daemon-reload
sudo systemctl enable itnews-web.service
sudo systemctl start itnews-web.service
# ステータス確認
sudo systemctl status itnews-web.service
おわりに
最新世代のGPU環境ではライブラリのバージョン整合性(特にPyTorch NightlyとCUDA toolkit)が重要ですが、環境を揃えることで約1,800件の推論・分類が数秒、再学習もわずか30秒足らずで完了する非常に快適なMLパイプラインが構築できました。
定期実行バッチ(cron/タスクスケジューラ)と組み合わせることで、運用すればするほど精度が向上するITニュースダッシュボードとして活用できます。